Build.PL100664023532023421 346212544604516 13136 0ustar00abrummetgsc000000000000UR-0.44# ========================================================================= # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. # DO NOT EDIT DIRECTLY. # ========================================================================= use 5.008_001; use strict; use warnings; use utf8; use builder::UR; use File::Basename; use File::Spec; use CPAN::Meta; use CPAN::Meta::Prereqs; my %args = ( license => 'perl', dynamic_config => 0, configure_requires => { 'Module::Build' => 0.38, }, name => 'UR', module_name => 'UR', allow_pureperl => 0, script_files => [glob('script/*'), glob('bin/*')], c_source => [qw()], PL_files => {}, test_files => ((-d '.git' || $ENV{RELEASE_TESTING}) && -d 'xt') ? 't/ xt/' : 't/', recursive_test_files => 1, ); if (-d 'share') { $args{share_dir} = 'share'; } my $builder = builder::UR->subclass( class => 'MyBuilder', code => q{ sub ACTION_distmeta { die "Do not run distmeta. Install Minilla and `minil install` instead.\n"; } sub ACTION_installdeps { die "Do not run installdeps. Run `cpanm --installdeps .` instead.\n"; } } )->new(%args); $builder->create_build_script(); my $mbmeta = CPAN::Meta->load_file('MYMETA.json'); my $meta = CPAN::Meta->load_file('META.json'); my $prereqs_hash = CPAN::Meta::Prereqs->new( $meta->prereqs )->with_merged_prereqs( CPAN::Meta::Prereqs->new($mbmeta->prereqs) )->as_string_hash; my $mymeta = CPAN::Meta->new( { %{$meta->as_struct}, prereqs => $prereqs_hash } ); print "Merging cpanfile prereqs to MYMETA.yml\n"; $mymeta->save('MYMETA.yml', { version => 1.4 }); print "Merging cpanfile prereqs to MYMETA.json\n"; $mymeta->save('MYMETA.json', { version => 2 }); Changes100664023532023421 4621612544604516 13161 0ustar00abrummetgsc000000000000UR-0.44Revision history for UR 0.44 2015-06-30T21:21:44Z Added UR::Context::AutoUnloadPool - a mechanism for automatically unloading objects when a leaving a scope. Added methods to UR::Object::Type to introspect methods names relating to is_many properties. The MetaDB no longer tracks owner/schema. Classes using tables not in the default schema should have their table_name listed as "schema.table". Added copy() constructor to UR::Object Removed old, deprecated filter parser for turning text into a UR::BoolExpr within UR::Object::Command::List Meta-params like -order_by are now allowed in a delegated property's where clase. Retrieving values for doubly-delegated properties is more efficient. An id-by delegated property can point to a class with multiple ID properties. The linking value is the composite ID. Observers now have a 'once' property. Setting it to true ensures the callback will only ever fire one time. The Observer is deleted. Properties can have a 'calculated_default' subref. Works like default_value, but the return value from the subref is the default value rather than have a hardcoded default. Fixes to work with newer versions of SQLite Added __rollback__() to UR::Object, called when the base Context rolls-back. Subclasses can override this to provide special behavior during rollback. The override should also call SUPER::__rollback__(). 0.43 2014-07-03T14:13:27Z Set objects now have member_iterator() method. Data loaded from an RDBMS during the life of a program can be copied to an alternate database. Use the "C" collation with PostgreSQL when doing an order-by on a text-type column to match how UR will sort cached objects using perl's cmp. Singleton accessors can be called on the class as well as the instance. Class initializer is more strict about what is a valid property name; it must be a valid function name. Added UR::Value::JSON class. Its "id" is a JSON-encoded string of the instance's properties and values. Added UR::Context::Transaction::eval() and do() functions to wrap software transactions around blocks. Added UR::DataSource::RDBMSRetriableOperations mixin class to allow RDBMSs to control whether failed DB operations should be retried. Added signals for when a data source fails a query or commit, and when the handle is created or disconnected. Added signal to UR::Context for when synchronizing to the datasources has succeeded or failed. Added 'isa' operator to boolean expressions. Evaluates to true if the attribute isa the given class. Fixed a bug where a Set object's value for an aggregate would be incorrect if cached member objects' values change. Fixed a bug where UR objects frozen in boolean expressions could cause database rows to be deleted when thawed. UR's class browser (ur sys class-browser) is working again. 0.42 2014-06-26T22:12:56Z Test releases to try out our new release system with minilla 0.41 2013-03-18 above.pm now imports symbols into the caller's package Fix for database connections after fork() in the child process Fixes for command-line parsing, implied property metadata and database joins Many test updates to work on more architectures 0.40 2013-02-25 RDBMS data sources now have infrastructure for comparing text and non-text columns during a join. When a number or date column is joined with a text column, the non-text column is converted with the to_char() function in the Oracle data source. An object-type property's default_value can now be specified using a hashref of keys/values. Property definitions can now include example_values - a listref of values shown to the user in the autogenerated documentation. Documentation for the Object Lister base command is expanded. 0.392 2013-01-31 Changed the name for the Yapp driver package to avoid a CPAN warning about unauthorized use of their namespace 0.39 2013-01-30 Better support for PostgreSQL. It is now on par with Oracle. New datasource UR::DataSource::Filesystem. It obsoletes UR::DataSource::File and UR::DataSource::FileMux, and is more flexible. Classes can specify a query hint when they are used as the primary class of a get() or when they are involved in a join. BoolExprs with an or-clause now support hints and order-by correctly. Messaging methods (error_message(), status_message(), etc) now trigger observers of the same name. This means any number of message observers can be attached at any point in the class hierarchy. Using chained delegated properties with the dot-syntax (object.delegate.prop) is accepted in more places. Better support for queries using direct SQL. Many fixes for the Boolean Expression syntax parser. Besides fixing bugs, it now supports more operators and understands 'offset' and 'limit'. Support for defining a property that is an alias for another. Fixes for remaining connected to databases after fork(). Optimization for the case where a delegation goes through an abstract class with no data source and back to the original data source. It now does one query instead of many. Improvements to the Command API documentation. Removed some deps on XML-related modules. 0.38 2012-03-28 Bug fixes to support C3 inheritance on the Mac correctly. Rich extensions to primitive/value data-types for files, etc. Optimization for very large in-clauses. Database updates now infer table structure from class meta-data instead of leaning on database metadata when inserting (update and delete already do this). Bug fixes to the new boolean expression parser. Fixes to complex inheritance in RDBMS data. Fix to sorting issues in older Perl 5.8. Bug fixes to boolean expressions with values which are non-UR objects Smarter query plans when the join table is variable (not supported in SQL, but in the API), leading to multiple database queries where necessary. 0.37 2012-02-03 Added a proper parser for generating Boolean Expressions from text strings. The object lister commands (UR::Object::Command::List) use it to process the --filter, and it can be used directly through the method UR::BoolExpr::resolve_for_string(). See the UR::BoolExpr pod for more info. Or-type Boolean Expressions now support -order, and can be the filter for iterators. Important Bugfixes: * Better error messages when a module fails to load properly during autoloading. * Class methods called on Set instances are dispatched to the proper class instead of called on the Set's members. * Values in an SQL in-clause are escaped using DBI's quote() method. 0.36 2012-01-05 Fix for 'like' clause's escape string on PostgreSQL Speed improvement for class initialization by normalizing metadata more efficiently and only calculating the cached data for property_meta_for_name() once. Workaround for a bug in Perl 5.8 involving sorters by avoiding method calls inside some sort subs Fully deprecate the old Subscription API in favor of the new Observer api UR::Value classes use UR::DataSource::Default and the normal loading mechanism. Previously, UR::Values used a special codepath to get loaded into memory Add a json perspective for available views Allow descending sorts in order-by. For example: my @o = Some::Class->get(prop => 'value', -order => ['field1','-field2'] To get all objects where prop is equal to the string 'value', first sorted by field1 in ascending order, then by field2 in descending order Standardize sorting results on columns with NULLs by having NULL/undef always appears at the end for ascending sorts. Previously, the order depended on the data source's behavior. Oracle and PostgreSQL put them at the end, while MySQL, SQLite and cached get()s put them at the beginning. Fix exit code for 'ur test run' when the --lsf arg is used. It used always return a false value (1). Now it returns true (0) if all tests pass, and false (1) if any one test fails. UR::Object now implements the messaging API that used to be in Command (error_message, dump_error_messages, etc). The old messaging API is now deprecated. 0.35 2011-10-28 Queries with the -recurse option are suppored for all datasources, not just those that support recursive queries directly Make the object listers more user-friendly by implicitly putting '%' wildcards on either side of the user-supplied 'like' filter Update to the latest version of Getopt::Complete for command-line completion Object Set fixes (non-datasource expressable filters) Bugfixes for queries involving multiple joins to the same table with different join conditions Queries with -offset/-limit and -page are now supported. Query efficiency improvements: * id_by properties with a know data_type have special code in the bridging logic to handle them more efficiently * large in-clause testing uses a binary search instead of linear for cached objects * no longer indexing delegated properties results in fewer unnecessary queries during loading * remove unnecessary rule evaluations against loaded objects * When a query includes a filter or -hints for a calculated property, implicitly add its calculate_from properties to the -hints list * Rules in the query cache are always normalized, which makes many lookups faster * Fix a bug where rules in the query cache related to in-clause queries were malformed, resulting in fewer queries to the data source Command module fixes: * running with --help no longer emits error messages about other missing params * Help output only lists properties that are is_input or is_param Deleted objects hanging around as UR::DeletedRefs are recycled if the original object gets re-created 0.34 2011-07-26 New class (Command::SubCommandFactory) which can act as a factory for a tree of sub-commands Remove the distinction between older and newer versions of DBD::SQLite installed on the system. If you have SQLite databases (including MetaDBs) with names like "*sqlite3n*", they will need to be renamed to "*sqlite3*". Make the tests emit fewer messages to the terminal when run in the harness; improve coverage on non-Intel/Linux systems. 0.33 2011-06-30 New environment variable (UR_DBI_SUMMARIZE_SQL) to help find query optimization targets View aspects for objects' primitive values use the appropriate UR::Value View classes Query engine remembers cases where a left join matches nothing, and skips asking the datasource on subsequent similar queries Committing a software transaction now performs the same data consistancy checking as the top-level transaction. Improved document auto-generation for Command classes Improved SQLite Data Source schema introspection Updated database handling for Pg and mysql table case sensitivity UR's developer tools (ur command-line tool) can operate on non-standard source tree layouts, and can be forced to operate on a namespace with a command-line option Support for using a chain of properties in queries ('a.b.c like' => $v) Set operations normalized: min, max, sum, count Set-to-set relaying is now correctly lazy Objects previously loaded from the database, and later deleted from the database, are now detected as deleted and handled as another type of change to be merged with in-memory changes. 0.32 (skipped) 0.31 (skipped) 0.30 2011-03-07 re-package 0.29 with versions correctly set 0.29 2011-03-07 query/iteration engine now solves n+1 in the one-to-many case as well as many-to-one query optimization where the join table is variable across rows in a single resultset automated manual page creation for commands reduced deps (removed UR::Time) 0.28 2011-01-23 fix to the installer which caused a failure during docs generation improvements to man page generation 0.27 2011-01-22 updated build process autogenerates man pages 0.26 2011-01-16 yet another refactoring to ensure VERSION appears on all modules fixes for tests which fail only in the install harness 0.25 2011-01-15 Updated docs. 0.24 2011-01-15 Updated deps to compile fully on a new OSX installation (requires XCode). 0.22 2011-01-12 VERSION refactoring for cleaner uploads 0.20 2011-01-11 faster compile (<.5s) faster object creation faster install documentation polish 0.19 2010-12-24 faster compile faster query cache resolution leaner meta-data less build deps, build dep fixes hideable commands fixes for newer sqlite API revamped UR::BoolExpr API new command tree 0.18 2010-12-10 Bugfix for queries involving subclasses without tables Preliminary support for building debian packages Bugfixes for queries with the 'in' and 'not in' operators Object cache indexing sped up by replacing regexes with direct string comparisons 0.17 2010-11-10 Fixed bug with default datasources dumping debug info during queries. Deprecated old parts of the UR::Object API. Bugfixes for MySQL data sources with handling of between and like operators, and table/column name case sensitivity MySQL data sources will complain if the 'lower_case_table_names' setting is not set to 1 Bugfixes for FileMux data sources to return objects from iterators in correct sorted order File data sources remember their file offsets more often to improve seeking Bugfixes for handling is_many values passed in during create() New class for JSON-formatted Set views More consistent behavior during evaluation of BoolExprs with is_many values and undef/NULL values Bugfixes for handling observers during software transaction commit and rollback Addition of a new UR::Change type (external_change) to track non-UR entities that need undo-ing during a rollback 0.16 2010-09-27 File datasources build an on-the-fly index to improve its ability to seek within the file Initial support for classes to supply custom logic for loading data Compile-time speed improvements Bug fixes for SQL generation with indirect properties, and the object cache pruner 0.15 2010-08-03 Improved 'ur update classes' interaction with MySQL databases Integration with Getopt::Complete for bash command-line tab completion 0.14 2010-07-26 Metadata about data source entities (tables, columns, etc) is autodiscovered within commit() if it doesn't already exist in the MetaDB The new View API now has working default toolkits for HTML, Text, XML and XSL. The old Viewer API has been removed. Smarter property merging when the Context reloads an already cached object and the data in the data source has changed Added a built-in 'product' calculation property type Calculated properties can now be memoized subclassify_by for an abstract class can now be a regular, indirect or calculated property New environment variable UR_CONTEXT_MONITOR_QUERY for printing Context/query info to stdout SQLite data sources can initialize themselves even if the sqlite3 executable cannot be found Test harness improvements: --junit and --color options, control-C stops tests and reports results 'use lib' within an autoloaded module stays in effect after the module is loaded 0.13 2010-02-21 Circular foreign key constraints between tables are now handled smartly handled in UR::DataSource::RDBMS. New meta-property properties: id_class_by, order_by, specify_by. Updated autogenerated Command documentation. Formalized the __extend_namespace__ callback for dynamic class creation. New Command::DynamicSubCommands class makes command trees for a group of classes easy. The new view API is available. The old "viewer" API is still available in this release, but is deprecated. 0.12 2009-09-09 'ur test run' can now run tests in parallel and can submit tests as jobs to LSF Command modules now have support for Getopt::Complete for bash tab-completion Bugfixes related to saving objects to File data sources. Several more fixes for merging between database and in-memory objects. Property names beginning with an underscore are now handled properly during rule and object creation 0.11 2009-07-30 Fix bug in merge between database/in-memory data sets with changes. 0.10 2009-07-22 Updates to the UR::Object::Type MOP documentation. Other documentation cleanup and file cleanup. 0.9 2009-06-19 Additional build fixes. 0.8 2009-06-17 David's build fixes. 0.7 2009-06-10 Fix to build process: the distribution will work if you do not have Module::Install installed. 0.6 2009-06-07 Fixed to build process: actually install the "ur" executable. 0.5 2009-06-06 Updates to POD. Additional API updates to UR::Object. 0.4 2009-06-04 Updates to POD. Extensive API updates to UR::Object. 0.3 2009-05-29 Fixed memory leak in cache pruner, and added additional debugging environment variable. Additional laziness on file-based data-sources. Updated lots of POD. Switched to version numbers without zero padding! 0.02 2009-05-23 Cleanup of initial deployment issues. UR uses a non-default version of Class::Autouse. This is now a special file to prevent problems with the old version. Links to old DBIx::Class modules are now gone. Updated boolean expression API. 0.01 2009-05-07 First public release for Lambda Lounge language shootout. INSTALL100664023532023421 7112544604516 12624 0ustar00abrummetgsc000000000000UR-0.44perl Build.PL ./Build ./Build test sudo ./Build install LICENSE100664023532023421 12126412544604516 12710 0ustar00abrummetgsc000000000000UR-0.44UR is licensed under the same terms as Perl itself, which means it is dually-licensed under either the Artistic or GPL licenses. Below are details of the Artistic License and, following it, the GPL. The "Artistic License" Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder as specified below. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You" is you, if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as uunet.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) give non-standard executables non-standard names, and clearly document the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. You may embed this Package's interpreter within an executable of yours (by linking); this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter is so embedded. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall it fall under the restrictions of Paragraphs 3 and 4, provided that you do not represent such an executable image as a Standard Version of this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6, provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded; that is, when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . META.json100664023532023421 12753612544604516 13334 0ustar00abrummetgsc000000000000UR-0.44{ "abstract" : "rich declarative transactional objects", "author" : [ "UR was built by the software development team at the McDonnell Genome" ], "dynamic_config" : 0, "generated_by" : "Minilla/v1.1.0", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "UR", "no_index" : { "directory" : [ "t", "xt", "inc", "share", "eg", "examples", "author", "builder" ] }, "prereqs" : { "configure" : { "requires" : { "CPAN::Meta" : "0", "CPAN::Meta::Prereqs" : "0", "Module::Build" : "0.38" } }, "develop" : { "requires" : { "CPAN::Uploader" : "0", "Minilla" : "v1.1.0", "Test::CPAN::Meta" : "0", "Test::MinimumVersion::Fast" : "0.04", "Test::PAUSE::Permissions" : "0.04", "Test::Pod" : "1.41", "Test::Spellunker" : "v0.2.7", "Version::Next" : "0" } }, "runtime" : { "requires" : { "Carp" : "0", "Class::AutoloadCAN" : "0.03", "Class::Autouse" : "2.0", "Clone::PP" : "1.02", "DBD::SQLite" : "1.14", "DBI" : "1.601", "Data::Compare" : "0.13", "Data::UUID" : "0.148", "Date::Format" : "0", "Devel::GlobalDestruction" : "0", "File::Basename" : "2.73", "File::Path" : "0", "File::Temp" : "0", "FreezeThaw" : "0.43", "Getopt::Complete" : "0.26", "HTTP::Request" : "0", "JSON" : "0", "Lingua::EN::Inflect" : "1.88", "List::MoreUtils" : "0", "MRO::Compat" : "0", "Module::Runtime" : "v0.14.0", "Path::Class" : "0", "Plack" : "0", "Pod::Simple::HTML" : "3.03", "Pod::Simple::Text" : "2.02", "Sub::Install" : "0.924", "Sub::Name" : "0.04", "Sys::Hostname" : "1.11", "Template" : "0", "Text::Diff" : "0.35", "Text::Glob" : "0", "YAML" : "0", "perl" : "v5.8.7", "version" : "0" } }, "test" : { "requires" : { "Test::Deep" : "0", "Test::Exception" : "0", "Test::Fatal" : "0", "Test::Fork" : "0", "Test::More" : "0.98" } } }, "provides" : { "Command" : { "file" : "lib/Command.pm", "version" : "0.44" }, "Command::Common" : { "file" : "lib/Command/Common.pm" }, "Command::DynamicSubCommands" : { "file" : "lib/Command/DynamicSubCommands.pm" }, "Command::Shell" : { "file" : "lib/Command/Shell.pm" }, "Command::SubCommandFactory" : { "file" : "lib/Command/SubCommandFactory.pm" }, "Command::Test" : { "file" : "lib/Command/Test.pm" }, "Command::Test::Echo" : { "file" : "lib/Command/Test/Echo.pm" }, "Command::Test::Tree1" : { "file" : "lib/Command/Test/Tree1.pm" }, "Command::Test::Tree1::Echo1" : { "file" : "lib/Command/Test/Tree1/Echo1.pm" }, "Command::Test::Tree1::Echo2" : { "file" : "lib/Command/Test/Tree1/Echo2.pm" }, "Command::Tree" : { "file" : "lib/Command/Tree.pm", "version" : "0.44" }, "Command::V1" : { "file" : "lib/Command/V1.pm", "version" : "0.44" }, "Command::V2" : { "file" : "lib/Command/V2.pm", "version" : "0.44" }, "Devel::callsfrom" : { "file" : "lib/Devel/callcount.pm" }, "My::TAP::Parser::Iterator::Process::LSF" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm" }, "My::TAP::Parser::IteratorFactory::LSF" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm" }, "My::TAP::Parser::Multiplexer" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm" }, "My::TAP::Parser::Scheduler" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm" }, "My::TAP::Parser::Timer" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm" }, "UR" : { "file" : "lib/UR.pm", "version" : "0.44" }, "UR::All" : { "file" : "lib/UR/All.pm", "version" : "0.44" }, "UR::BoolExpr" : { "file" : "lib/UR/BoolExpr.pm", "version" : "0.44" }, "UR::BoolExpr::BxParser" : { "file" : "lib/UR/BoolExpr/BxParser.pm" }, "UR::BoolExpr::BxParser::Yapp::Driver" : { "file" : "lib/UR/BoolExpr/BxParser.pm", "version" : "1.05" }, "UR::BoolExpr::Template" : { "file" : "lib/UR/BoolExpr/Template.pm", "version" : "0.44" }, "UR::BoolExpr::Template::And" : { "file" : "lib/UR/BoolExpr/Template/And.pm", "version" : "0.44" }, "UR::BoolExpr::Template::Composite" : { "file" : "lib/UR/BoolExpr/Template/Composite.pm", "version" : "0.44" }, "UR::BoolExpr::Template::Or" : { "file" : "lib/UR/BoolExpr/Template/Or.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::Between" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Between.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::Equals" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::False" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/False.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::GreaterThan" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::In" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/In.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::Isa" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Isa.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::LessOrEqual" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::LessThan" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::Like" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Like.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::Matches" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::NotBetween" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotBetween.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::NotEquals" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotEquals.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::NotIn" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::NotLike" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm", "version" : "0.44" }, "UR::BoolExpr::Template::PropertyComparison::True" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/True.pm", "version" : "0.44" }, "UR::BoolExpr::Util" : { "file" : "lib/UR/BoolExpr/Util.pm", "version" : "0.44" }, "UR::BoolExpr::Util::clonedThing" : { "file" : "lib/UR/BoolExpr/Util.pm" }, "UR::Change" : { "file" : "lib/UR/Change.pm", "version" : "0.44" }, "UR::Context" : { "file" : "lib/UR/Context.pm", "version" : "0.44" }, "UR::Context::AutoUnloadPool" : { "file" : "lib/UR/Context/AutoUnloadPool.pm", "version" : "0.44" }, "UR::Context::DefaultRoot" : { "file" : "lib/UR/Context/DefaultRoot.pm", "version" : "0.44" }, "UR::Context::LoadingIterator" : { "file" : "lib/UR/Context/LoadingIterator.pm", "version" : "0.44" }, "UR::Context::ObjectFabricator" : { "file" : "lib/UR/Context/ObjectFabricator.pm", "version" : "0.44" }, "UR::Context::Process" : { "file" : "lib/UR/Context/Process.pm", "version" : "0.44" }, "UR::Context::Root" : { "file" : "lib/UR/Context/Root.pm", "version" : "0.44" }, "UR::Context::Transaction" : { "file" : "lib/UR/Context/Transaction.pm", "version" : "0.44" }, "UR::DBI" : { "file" : "lib/UR/DBI.pm", "version" : "0.44" }, "UR::DBI::Report" : { "file" : "lib/UR/DBI/Report.pm", "version" : "0.44" }, "UR::DBI::db" : { "file" : "lib/UR/DBI.pm" }, "UR::DBI::st" : { "file" : "lib/UR/DBI.pm" }, "UR::DataSource" : { "file" : "lib/UR/DataSource.pm", "version" : "0.44" }, "UR::DataSource::CSV" : { "file" : "lib/UR/DataSource/CSV.pm", "version" : "0.44" }, "UR::DataSource::Code" : { "file" : "lib/UR/DataSource/Code.pm", "version" : "0.44" }, "UR::DataSource::Default" : { "file" : "lib/UR/DataSource/Default.pm", "version" : "0.44" }, "UR::DataSource::File" : { "file" : "lib/UR/DataSource/File.pm", "version" : "0.44" }, "UR::DataSource::FileMux" : { "file" : "lib/UR/DataSource/FileMux.pm", "version" : "0.44" }, "UR::DataSource::Filesystem" : { "file" : "lib/UR/DataSource/Filesystem.pm", "version" : "0.44" }, "UR::DataSource::Meta" : { "file" : "lib/UR/DataSource/Meta.pm", "version" : "0.44" }, "UR::DataSource::MySQL" : { "file" : "lib/UR/DataSource/MySQL.pm", "version" : "0.44" }, "UR::DataSource::Oracle" : { "file" : "lib/UR/DataSource/Oracle.pm", "version" : "0.44" }, "UR::DataSource::Pg" : { "file" : "lib/UR/DataSource/Pg.pm", "version" : "0.44" }, "UR::DataSource::Pg::Operator::False" : { "file" : "lib/UR/DataSource/Pg/Operator/False.pm" }, "UR::DataSource::Pg::Operator::True" : { "file" : "lib/UR/DataSource/Pg/Operator/True.pm" }, "UR::DataSource::QueryPlan" : { "file" : "lib/UR/DataSource/QueryPlan.pm", "version" : "0.44" }, "UR::DataSource::RDBMS" : { "file" : "lib/UR/DataSource/RDBMS.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::BitmapIndex" : { "file" : "lib/UR/DataSource/RDBMS/BitmapIndex.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::Entity" : { "file" : "lib/UR/DataSource/RDBMS/Entity.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::FkConstraint" : { "file" : "lib/UR/DataSource/RDBMS/FkConstraint.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::FkConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/FkConstraintColumn.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::Operator::Between" : { "file" : "lib/UR/DataSource/RDBMS/Operator/Between.pm" }, "UR::DataSource::RDBMS::Operator::Equals" : { "file" : "lib/UR/DataSource/RDBMS/Operator/Equals.pm" }, "UR::DataSource::RDBMS::Operator::False" : { "file" : "lib/UR/DataSource/RDBMS/Operator/False.pm" }, "UR::DataSource::RDBMS::Operator::GreaterOrEqual" : { "file" : "lib/UR/DataSource/RDBMS/Operator/GreaterOrEqual.pm" }, "UR::DataSource::RDBMS::Operator::GreaterThan" : { "file" : "lib/UR/DataSource/RDBMS/Operator/GreaterThan.pm" }, "UR::DataSource::RDBMS::Operator::In" : { "file" : "lib/UR/DataSource/RDBMS/Operator/In.pm" }, "UR::DataSource::RDBMS::Operator::LessOrEqual" : { "file" : "lib/UR/DataSource/RDBMS/Operator/LessOrEqual.pm" }, "UR::DataSource::RDBMS::Operator::LessThan" : { "file" : "lib/UR/DataSource/RDBMS/Operator/LessThan.pm" }, "UR::DataSource::RDBMS::Operator::Like" : { "file" : "lib/UR/DataSource/RDBMS/Operator/Like.pm" }, "UR::DataSource::RDBMS::Operator::NotBetween" : { "file" : "lib/UR/DataSource/RDBMS/Operator/NotBetween.pm" }, "UR::DataSource::RDBMS::Operator::NotEquals" : { "file" : "lib/UR/DataSource/RDBMS/Operator/NotEquals.pm" }, "UR::DataSource::RDBMS::Operator::NotIn" : { "file" : "lib/UR/DataSource/RDBMS/Operator/NotIn.pm" }, "UR::DataSource::RDBMS::Operator::NotLike" : { "file" : "lib/UR/DataSource/RDBMS/Operator/NotLike.pm" }, "UR::DataSource::RDBMS::Operator::True" : { "file" : "lib/UR/DataSource/RDBMS/Operator/True.pm" }, "UR::DataSource::RDBMS::PkConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/PkConstraintColumn.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::Table" : { "file" : "lib/UR/DataSource/RDBMS/Table.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::Table::View::Default::Text" : { "file" : "lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::TableColumn" : { "file" : "lib/UR/DataSource/RDBMS/TableColumn.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::TableColumn::View::Default::Text" : { "file" : "lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm", "version" : "0.44" }, "UR::DataSource::RDBMS::UniqueConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm", "version" : "0.44" }, "UR::DataSource::RDBMSRetriableOperations" : { "file" : "lib/UR/DataSource/RDBMSRetriableOperations.pm" }, "UR::DataSource::SQLite" : { "file" : "lib/UR/DataSource/SQLite.pm", "version" : "0.44" }, "UR::DataSource::ValueDomain" : { "file" : "lib/UR/DataSource/ValueDomain.pm", "version" : "0.44" }, "UR::Debug" : { "file" : "lib/UR/Debug.pm", "version" : "0.44" }, "UR::DeletedRef" : { "file" : "lib/UR/DeletedRef.pm", "version" : "0.44" }, "UR::Doc::Pod2Html" : { "file" : "lib/UR/Doc/Pod2Html.pm", "version" : "0.44" }, "UR::Doc::Section" : { "file" : "lib/UR/Doc/Section.pm", "version" : "0.44" }, "UR::Doc::Writer" : { "file" : "lib/UR/Doc/Writer.pm", "version" : "0.44" }, "UR::Doc::Writer::Html" : { "file" : "lib/UR/Doc/Writer/Html.pm", "version" : "0.44" }, "UR::Doc::Writer::Pod" : { "file" : "lib/UR/Doc/Writer/Pod.pm", "version" : "0.44" }, "UR::Env::UR_COMMAND_DUMP_DEBUG_MESSAGES" : { "file" : "lib/UR/Env/UR_COMMAND_DUMP_DEBUG_MESSAGES.pm", "version" : "0.44" }, "UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES" : { "file" : "lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_BASE" : { "file" : "lib/UR/Env/UR_CONTEXT_BASE.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER" : { "file" : "lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER" : { "file" : "lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_LIBS" : { "file" : "lib/UR/Env/UR_USED_LIBS.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_MONITOR_QUERY" : { "file" : "lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm", "version" : "0.44" }, "UR::Env::UR_CONTEXT_ROOT" : { "file" : "lib/UR/Env/UR_CONTEXT_ROOT.pm", "version" : "0.44" }, "UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT" : { "file" : "lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm", "version" : "0.44" }, "UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm", "version" : "0.44" }, "UR::Env::UR_DBI_EXPLAIN_SQL_IF" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm", "version" : "0.44" }, "UR::Env::UR_DBI_EXPLAIN_SQL_MATCH" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm", "version" : "0.44" }, "UR::Env::UR_DBI_EXPLAIN_SQL_SLOW" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm", "version" : "0.44" }, "UR::Env::UR_DBI_MONITOR_DML" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_DML.pm", "version" : "0.44" }, "UR::Env::UR_DBI_MONITOR_EVERY_FETCH" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm", "version" : "0.44" }, "UR::Env::UR_DBI_MONITOR_SQL" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_SQL.pm", "version" : "0.44" }, "UR::Env::UR_DBI_NO_COMMIT" : { "file" : "lib/UR/Env/UR_DBI_NO_COMMIT.pm", "version" : "0.44" }, "UR::Env::UR_DBI_SUMMARIZE_SQL" : { "file" : "lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm", "version" : "0.44" }, "UR::Env::UR_DEBUG_OBJECT_PRUNING" : { "file" : "lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm", "version" : "0.44" }, "UR::Env::UR_DEBUG_OBJECT_RELEASE" : { "file" : "lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm", "version" : "0.44" }, "UR::Env::UR_DUMP_DEBUG_MESSAGES" : { "file" : "lib/UR/Env/UR_DUMP_DEBUG_MESSAGES.pm", "version" : "0.44" }, "UR::Env::UR_DUMP_STATUS_MESSAGES" : { "file" : "lib/UR/Env/UR_DUMP_STATUS_MESSAGES.pm", "version" : "0.44" }, "UR::Env::UR_IGNORE" : { "file" : "lib/UR/Env/UR_IGNORE.pm", "version" : "0.44" }, "UR::Env::UR_NO_REQUIRE_USER_VERIFY" : { "file" : "lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm", "version" : "0.44" }, "UR::Env::UR_NR_CPU" : { "file" : "lib/UR/Env/UR_NR_CPU.pm", "version" : "0.44" }, "UR::Env::UR_RUN_LONG_TESTS" : { "file" : "lib/UR/Env/UR_RUN_LONG_TESTS.pm", "version" : "0.44" }, "UR::Env::UR_STACK_DUMP_ON_DIE" : { "file" : "lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm", "version" : "0.44" }, "UR::Env::UR_STACK_DUMP_ON_WARN" : { "file" : "lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm", "version" : "0.44" }, "UR::Env::UR_TEST_QUIET" : { "file" : "lib/UR/Env/UR_TEST_QUIET.pm", "version" : "0.44" }, "UR::Env::UR_USED_MODS" : { "file" : "lib/UR/Env/UR_USED_MODS.pm", "version" : "0.44" }, "UR::Env::UR_USE_ANY" : { "file" : "lib/UR/Env/UR_USE_ANY.pm", "version" : "0.44" }, "UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS" : { "file" : "lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm", "version" : "0.44" }, "UR::Exit" : { "file" : "lib/UR/Exit.pm", "version" : "0.44" }, "UR::ModuleBase" : { "file" : "lib/UR/ModuleBase.pm", "version" : "0.44" }, "UR::ModuleBase::Message" : { "file" : "lib/UR/ObjectDeprecated.pm" }, "UR::ModuleBuild" : { "file" : "lib/UR/ModuleBuild.pm" }, "UR::ModuleConfig" : { "file" : "lib/UR/ModuleConfig.pm", "version" : "0.44" }, "UR::ModuleLoader" : { "file" : "lib/UR/ModuleLoader.pm", "version" : "0.44" }, "UR::Namespace" : { "file" : "lib/UR/Namespace.pm", "version" : "0.44" }, "UR::Namespace::Command" : { "file" : "lib/UR/Namespace/Command.pm", "version" : "0.44" }, "UR::Namespace::Command::Base" : { "file" : "lib/UR/Namespace/Command/Base.pm", "version" : "0.44" }, "UR::Namespace::Command::Define" : { "file" : "lib/UR/Namespace/Command/Define.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Class" : { "file" : "lib/UR/Namespace/Command/Define/Class.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource" : { "file" : "lib/UR/Namespace/Command/Define/Datasource.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::File" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/File.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::Mysql" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Mysql.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::Oracle" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Oracle.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::Pg" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Pg.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::Rdbms" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Datasource::Sqlite" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Db" : { "file" : "lib/UR/Namespace/Command/Define/Db.pm", "version" : "0.44" }, "UR::Namespace::Command::Define::Namespace" : { "file" : "lib/UR/Namespace/Command/Define/Namespace.pm", "version" : "0.44" }, "UR::Namespace::Command::Init" : { "file" : "lib/UR/Namespace/Command/Init.pm", "version" : "0.44" }, "UR::Namespace::Command::List" : { "file" : "lib/UR/Namespace/Command/List.pm", "version" : "0.44" }, "UR::Namespace::Command::List::Classes" : { "file" : "lib/UR/Namespace/Command/List/Classes.pm", "version" : "0.44" }, "UR::Namespace::Command::List::Modules" : { "file" : "lib/UR/Namespace/Command/List/Modules.pm", "version" : "0.44" }, "UR::Namespace::Command::List::Objects" : { "file" : "lib/UR/Namespace/Command/List/Objects.pm", "version" : "0.44" }, "UR::Namespace::Command::Old" : { "file" : "lib/UR/Namespace/Command/Old.pm", "version" : "0.44" }, "UR::Namespace::Command::Old::DiffRewrite" : { "file" : "lib/UR/Namespace/Command/Old/DiffRewrite.pm", "version" : "0.44" }, "UR::Namespace::Command::Old::DiffUpdate" : { "file" : "lib/UR/Namespace/Command/Old/DiffUpdate.pm", "version" : "0.44" }, "UR::Namespace::Command::Old::ExportDbicClasses" : { "file" : "lib/UR/Namespace/Command/Old/ExportDbicClasses.pm", "version" : "0.44" }, "UR::Namespace::Command::Old::Info" : { "file" : "lib/UR/Namespace/Command/Old/Info.pm", "version" : "0.44" }, "UR::Namespace::Command::Old::Redescribe" : { "file" : "lib/UR/Namespace/Command/Old/Redescribe.pm", "version" : "0.44" }, "UR::Namespace::Command::RunsOnModulesInTree" : { "file" : "lib/UR/Namespace/Command/RunsOnModulesInTree.pm", "version" : "0.44" }, "UR::Namespace::Command::Show" : { "file" : "lib/UR/Namespace/Command/Show.pm" }, "UR::Namespace::Command::Show::Properties" : { "file" : "lib/UR/Namespace/Command/Show/Properties.pm", "version" : "0.44" }, "UR::Namespace::Command::Show::Schema" : { "file" : "lib/UR/Namespace/Command/Show/Schema.pm" }, "UR::Namespace::Command::Show::Subclasses" : { "file" : "lib/UR/Namespace/Command/Show/Subclasses.pm" }, "UR::Namespace::Command::Sys" : { "file" : "lib/UR/Namespace/Command/Sys.pm", "version" : "0.44" }, "UR::Namespace::Command::Sys::ClassBrowser" : { "file" : "lib/UR/Namespace/Command/Sys/ClassBrowser.pm", "version" : "0.44" }, "UR::Namespace::Command::Sys::ClassBrowser::TreeItem" : { "file" : "lib/UR/Namespace/Command/Sys/ClassBrowser.pm" }, "UR::Namespace::Command::Test" : { "file" : "lib/UR/Namespace/Command/Test.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Callcount" : { "file" : "lib/UR/Namespace/Command/Test/Callcount.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Callcount::List" : { "file" : "lib/UR/Namespace/Command/Test/Callcount/List.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Compile" : { "file" : "lib/UR/Namespace/Command/Test/Compile.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Eval" : { "file" : "lib/UR/Namespace/Command/Test/Eval.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Run" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::TrackObjectRelease" : { "file" : "lib/UR/Namespace/Command/Test/TrackObjectRelease.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Use" : { "file" : "lib/UR/Namespace/Command/Test/Use.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Window" : { "file" : "lib/UR/Namespace/Command/Test/Window.pm", "version" : "0.44" }, "UR::Namespace::Command::Test::Window::Tk" : { "file" : "lib/UR/Namespace/Command/Test/Window.pm" }, "UR::Namespace::Command::Update" : { "file" : "lib/UR/Namespace/Command/Update.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::ClassDiagram" : { "file" : "lib/UR/Namespace/Command/Update/ClassDiagram.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::ClassesFromDb" : { "file" : "lib/UR/Namespace/Command/Update/ClassesFromDb.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::Doc" : { "file" : "lib/UR/Namespace/Command/Update/Doc.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::Pod" : { "file" : "lib/UR/Namespace/Command/Update/Pod.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::RenameClass" : { "file" : "lib/UR/Namespace/Command/Update/RenameClass.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::RewriteClassHeader" : { "file" : "lib/UR/Namespace/Command/Update/RewriteClassHeader.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::SchemaDiagram" : { "file" : "lib/UR/Namespace/Command/Update/SchemaDiagram.pm", "version" : "0.44" }, "UR::Namespace::Command::Update::TabCompletionSpec" : { "file" : "lib/UR/Namespace/Command/Update/TabCompletionSpec.pm", "version" : "0.44" }, "UR::Object" : { "file" : "lib/UR/Object.pm", "version" : "0.44" }, "UR::Object::Accessorized" : { "file" : "lib/UR/Object/Accessorized.pm", "version" : "0.44" }, "UR::Object::Command::FetchAndDo" : { "file" : "lib/UR/Object/Command/FetchAndDo.pm", "version" : "0.44" }, "UR::Object::Command::List" : { "file" : "lib/UR/Object/Command/List.pm", "version" : "0.44" }, "UR::Object::Command::List::Csv" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Html" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Newtext" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Pretty" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Style" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : "0.44" }, "UR::Object::Command::List::Text" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Tsv" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Command::List::Xml" : { "file" : "lib/UR/Object/Command/List/Style.pm" }, "UR::Object::Ghost" : { "file" : "lib/UR/Object/Ghost.pm", "version" : "0.44" }, "UR::Object::Index" : { "file" : "lib/UR/Object/Index.pm", "version" : "0.44" }, "UR::Object::Iterator" : { "file" : "lib/UR/Object/Iterator.pm", "version" : "0.44" }, "UR::Object::Join" : { "file" : "lib/UR/Object/Join.pm", "version" : "0.44" }, "UR::Object::Property" : { "file" : "lib/UR/Object/Property.pm", "version" : "0.44" }, "UR::Object::Property::View::Default::Text" : { "file" : "lib/UR/Object/Property/View/Default/Text.pm", "version" : "0.44" }, "UR::Object::Property::View::DescriptionLineItem::Text" : { "file" : "lib/UR/Object/Property/View/DescriptionLineItem/Text.pm", "version" : "0.44" }, "UR::Object::Property::View::ReferenceDescription::Text" : { "file" : "lib/UR/Object/Property/View/ReferenceDescription/Text.pm", "version" : "0.44" }, "UR::Object::Set" : { "file" : "lib/UR/Object/Set.pm", "version" : "0.44" }, "UR::Object::Set::View::Default::Html" : { "file" : "lib/UR/Object/Set/View/Default/Html.pm", "version" : "0.44" }, "UR::Object::Set::View::Default::Json" : { "file" : "lib/UR/Object/Set/View/Default/Json.pm", "version" : "0.44" }, "UR::Object::Set::View::Default::Text" : { "file" : "lib/UR/Object/Set/View/Default/Text.pm", "version" : "0.44" }, "UR::Object::Set::View::Default::Xml" : { "file" : "lib/UR/Object/Set/View/Default/Xml.pm", "version" : "0.44" }, "UR::Object::Tag" : { "file" : "lib/UR/Object/Tag.pm", "version" : "0.44" }, "UR::Object::Type" : { "file" : "lib/UR/Object/Type.pm", "version" : "0.44" }, "UR::Object::Type::AccessorWriter" : { "file" : "lib/UR/Object/Type/AccessorWriter.pm" }, "UR::Object::Type::AccessorWriter::Product" : { "file" : "lib/UR/Object/Type/AccessorWriter/Product.pm", "version" : "0.44" }, "UR::Object::Type::AccessorWriter::Sum" : { "file" : "lib/UR/Object/Type/AccessorWriter/Sum.pm", "version" : "0.44" }, "UR::Object::Type::Initializer" : { "file" : "lib/UR/Object/Type/Initializer.pm" }, "UR::Object::Type::ModuleWriter" : { "file" : "lib/UR/Object/Type/ModuleWriter.pm" }, "UR::Object::Type::View::AvailableViews::Json" : { "file" : "lib/UR/Object/Type/View/AvailableViews/Json.pm", "version" : "0.44" }, "UR::Object::Type::View::AvailableViews::Xml" : { "file" : "lib/UR/Object/Type/View/AvailableViews/Xml.pm", "version" : "0.44" }, "UR::Object::Type::View::Default::Text" : { "file" : "lib/UR/Object/Type/View/Default/Text.pm", "version" : "0.44" }, "UR::Object::Type::View::Default::Xml" : { "file" : "lib/UR/Object/Type/View/Default/Xml.pm", "version" : "0.44" }, "UR::Object::Value" : { "file" : "lib/UR/Object/Value.pm", "version" : "0.44" }, "UR::Object::View" : { "file" : "lib/UR/Object/View.pm", "version" : "0.44" }, "UR::Object::View::Aspect" : { "file" : "lib/UR/Object/View/Aspect.pm", "version" : "0.44" }, "UR::Object::View::Default::Gtk" : { "file" : "lib/UR/Object/View/Default/Gtk.pm", "version" : "0.44" }, "UR::Object::View::Default::Gtk2" : { "file" : "lib/UR/Object/View/Default/Gtk2.pm", "version" : "0.44" }, "UR::Object::View::Default::Html" : { "file" : "lib/UR/Object/View/Default/Html.pm", "version" : "0.44" }, "UR::Object::View::Default::Json" : { "file" : "lib/UR/Object/View/Default/Json.pm", "version" : "0.44" }, "UR::Object::View::Default::Text" : { "file" : "lib/UR/Object/View/Default/Text.pm", "version" : "0.44" }, "UR::Object::View::Default::Xml" : { "file" : "lib/UR/Object/View/Default/Xml.pm", "version" : "0.44" }, "UR::Object::View::Default::Xsl" : { "file" : "lib/UR/Object/View/Default/Xsl.pm", "version" : "0.44" }, "UR::Object::View::Lister::Text" : { "file" : "lib/UR/Object/View/Lister/Text.pm", "version" : "0.44" }, "UR::Object::View::Static::Html" : { "file" : "lib/UR/Object/View/Static/Html.pm", "version" : "0.44" }, "UR::Object::View::Toolkit" : { "file" : "lib/UR/Object/View/Toolkit.pm", "version" : "0.44" }, "UR::Object::View::Toolkit::Text" : { "file" : "lib/UR/Object/View/Toolkit/Text.pm", "version" : "0.44" }, "UR::Observer" : { "file" : "lib/UR/Observer.pm", "version" : "0.44" }, "UR::Service::JsonRpcServer" : { "file" : "lib/UR/Service/JsonRpcServer.pm", "version" : "0.44" }, "UR::Service::RPC::Executer" : { "file" : "lib/UR/Service/RPC/Executer.pm", "version" : "0.44" }, "UR::Service::RPC::Message" : { "file" : "lib/UR/Service/RPC/Message.pm", "version" : "0.44" }, "UR::Service::RPC::Server" : { "file" : "lib/UR/Service/RPC/Server.pm", "version" : "0.44" }, "UR::Service::RPC::TcpConnectionListener" : { "file" : "lib/UR/Service/RPC/TcpConnectionListener.pm", "version" : "0.44" }, "UR::Service::UrlRouter" : { "file" : "lib/UR/Service/UrlRouter.pm" }, "UR::Service::WebServer" : { "file" : "lib/UR/Service/WebServer.pm" }, "UR::Service::WebServer::Server" : { "file" : "lib/UR/Service/WebServer/Server.pm" }, "UR::Singleton" : { "file" : "lib/UR/Singleton.pm", "version" : "0.44" }, "UR::Test" : { "file" : "lib/UR/Test.pm" }, "UR::Util" : { "file" : "lib/UR/Util.pm", "version" : "0.44" }, "UR::Util::ArrayRefIterator" : { "file" : "lib/UR/Util/ArrayRefIterator.pm" }, "UR::Value" : { "file" : "lib/UR/Value.pm", "version" : "0.44" }, "UR::Value::ARRAY" : { "file" : "lib/UR/Value/ARRAY.pm", "version" : "0.44" }, "UR::Value::Blob" : { "file" : "lib/UR/Value/Blob.pm", "version" : "0.44" }, "UR::Value::Boolean" : { "file" : "lib/UR/Value/Boolean.pm", "version" : "0.44" }, "UR::Value::Boolean::View::Default::Text" : { "file" : "lib/UR/Value/Boolean/View/Default/Text.pm", "version" : "0.44" }, "UR::Value::CODE" : { "file" : "lib/UR/Value/CODE.pm", "version" : "0.44" }, "UR::Value::CSV" : { "file" : "lib/UR/Value/CSV.pm", "version" : "0.44" }, "UR::Value::DateTime" : { "file" : "lib/UR/Value/DateTime.pm", "version" : "0.44" }, "UR::Value::Decimal" : { "file" : "lib/UR/Value/Decimal.pm", "version" : "0.44" }, "UR::Value::DirectoryPath" : { "file" : "lib/UR/Value/DirectoryPath.pm", "version" : "0.44" }, "UR::Value::FOF" : { "file" : "lib/UR/Value/FOF.pm", "version" : "0.44" }, "UR::Value::FilePath" : { "file" : "lib/UR/Value/FilePath.pm", "version" : "0.44" }, "UR::Value::FilesystemPath" : { "file" : "lib/UR/Value/FilesystemPath.pm", "version" : "0.44" }, "UR::Value::Float" : { "file" : "lib/UR/Value/Float.pm", "version" : "0.44" }, "UR::Value::GLOB" : { "file" : "lib/UR/Value/GLOB.pm", "version" : "0.44" }, "UR::Value::HASH" : { "file" : "lib/UR/Value/HASH.pm", "version" : "0.44" }, "UR::Value::Integer" : { "file" : "lib/UR/Value/Integer.pm", "version" : "0.44" }, "UR::Value::Iterator" : { "file" : "lib/UR/Value/Iterator.pm", "version" : "0.44" }, "UR::Value::JSON" : { "file" : "lib/UR/Value/JSON.pm" }, "UR::Value::Number" : { "file" : "lib/UR/Value/Number.pm", "version" : "0.44" }, "UR::Value::PerlReference" : { "file" : "lib/UR/Value/PerlReference.pm", "version" : "0.44" }, "UR::Value::REF" : { "file" : "lib/UR/Value/REF.pm", "version" : "0.44" }, "UR::Value::SCALAR" : { "file" : "lib/UR/Value/SCALAR.pm", "version" : "0.44" }, "UR::Value::Set" : { "file" : "lib/UR/Value/Set.pm", "version" : "0.44" }, "UR::Value::SloppyPrimitive" : { "file" : "lib/UR/Value/SloppyPrimitive.pm", "version" : "0.44" }, "UR::Value::String" : { "file" : "lib/UR/Value/String.pm", "version" : "0.44" }, "UR::Value::Text" : { "file" : "lib/UR/Value/Text.pm", "version" : "0.44" }, "UR::Value::Timestamp" : { "file" : "lib/UR/Value/Timestamp.pm", "version" : "0.44" }, "UR::Value::Type" : { "file" : "lib/UR/Value.pm" }, "UR::Value::URL" : { "file" : "lib/UR/Value/URL.pm", "version" : "0.44" }, "UR::Value::View::Default::Html" : { "file" : "lib/UR/Value/View/Default/Html.pm" }, "UR::Value::View::Default::Json" : { "file" : "lib/UR/Value/View/Default/Json.pm" }, "UR::Value::View::Default::Text" : { "file" : "lib/UR/Value/View/Default/Text.pm" }, "UR::Value::View::Default::Xml" : { "file" : "lib/UR/Value/View/Default/Xml.pm" }, "UR::Vocabulary" : { "file" : "lib/UR/Vocabulary.pm", "version" : "0.44" }, "above" : { "file" : "lib/above.pm", "version" : "0.03" }, "class_name" : { "file" : "lib/UR/Object/Type/Initializer.pm", "version" : "2" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/genome/UR/issues" }, "homepage" : "https://github.com/genome/UR", "repository" : { "url" : "git://github.com/genome/UR.git", "web" : "https://github.com/genome/UR" } }, "version" : "0.44", "x_contributors" : [ "josborne ", "thepler ", "charris ", "jweible ", "edemello ", "jschindl ", "mjohnson ", "ebelter ", "lcarmich ", "jwalker ", "gsanders ", "eclark ", "adukes ", "iferguso ", "ssmith ", "boberkfe ", "Mark Johnson ", "Ben Oberkfell ", "ssmith ", "Edward Belter ", "Feiyu Du ", "Edward Belter ", "Feiyu Du ", "ssmith ", "ssmith ", "Robert Long ", "Robert Long ", "Thomas Mooney ", "Eric Clark ", "Thomas Mooney ", "ssmith ", "Scott Smith X ", "eclark ", "Anthony Brummett ", "Rob Long ", "Adam Dukes ", "Eddie Belter ", "Kyung Kim ", "Joshua McMichael ", "Jim Weible ", "Philip Kimmey ", "Matt Callaway ", "Travis Abbott ", "Chris Oliver ", "Steven Wallace ", "James Koval ", "Gabriel Sanderson ", "Eddie Belter ", "Scott Smith ", "Brian Derickson ", "Justin Lolofie ", "Ian Ferguson ", "Neil Bowers ", "Scott Smith ", "Ben Oberkfell ", "Jason Walker ", "Mark Burnett ", "Scott Smith ", "Adam Coffman ", "Susanna Siebert ", "apregier ", "David Morton ", "David Morton ", "Sebastian Heil <(none)>", "APipe Tester ", "nnutter ", "Nathaniel Nutter ", "Thomas B. Mooney ", "Anthony Brummett ", "Anthony Brummett " ] } README.md100664023532023421 2222712544604516 13141 0ustar00abrummetgsc000000000000UR-0.44[![Build Status](https://travis-ci.org/genome/UR.png?branch=master)](https://travis-ci.org/genome/UR) # NAME UR - rich declarative transactional objects # VERSION This document describes UR version 0.44 # SYNOPSIS use UR; ## no database class Foo { is => 'Bar', has => [qw/prop1 prop2 prop3/] }; $o1 = Foo->create(prop1 => 111, prop2 => 222, prop3 => 333); @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object $o1->delete; @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns zero objects @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object again ## database class Animal { has => [ favorite_food => { is => 'Text', doc => "what's yummy?" }, ], data_source => 'MyDB1', table_name => 'Animal' }; class Cat { is => 'Animal', has => [ feet => { is => 'Number', default_value => 4 }, fur => { is => 'Text', valid_values => [qw/fluffy scruffy/] }, ], data_source => 'MyDB1', table_name => 'Cat' }; Cat->create(feet => 4, fur => 'fluffy', favorite_food => 'taters'); @cats = Cat->get(favorite_food => ['taters','sea bass']); $c = $cats[0]; print $c->feet,"\n"; $c->fur('scruffy'); UR::Context->commit(); # DESCRIPTION UR is a class framework and object/relational mapper for Perl. It starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and extends its capabilities with ORM (object-relational mapping) capabilities, object cache, in-memory transactions, more formal class definitions, metadata, documentation system, iterators, command line tools, etc. UR can handle multiple column primary and foreign keys, SQL joins involving class inheritance and relationships, and does its best to avoid querying the database unless the requested data has not been loaded before. It has support for SQLite, Oracle, Mysql and Postgres databases, and the ability to use a text file as a table. UR uses the same syntax to define non-persistent objects, and supports in-memory transactions for both. # DOCUMENTATION ## Manuals [ur](https://metacpan.org/pod/ur) - command line interface [UR::Manual::Overview](https://metacpan.org/pod/UR::Manual::Overview) - UR from Ten Thousand Feet [UR::Manual::Tutorial](https://metacpan.org/pod/UR::Manual::Tutorial) - Getting started with UR [UR::Manual::Presentation](https://metacpan.org/pod/UR::Manual::Presentation) - Slides for a presentation on UR [UR::Manual::Cookbook](https://metacpan.org/pod/UR::Manual::Cookbook) - Recepies for getting stuff working [UR::Manual::Metadata](https://metacpan.org/pod/UR::Manual::Metadata) - UR's metadata system [UR::Object::Type::Initializer](https://metacpan.org/pod/UR::Object::Type::Initializer) - Defining classes ## Basic Entities [UR::Object](https://metacpan.org/pod/UR::Object) - Pretty much everything is-a UR::Object [UR::Object::Type](https://metacpan.org/pod/UR::Object::Type) - Metadata class for Classes [UR::Object::Property](https://metacpan.org/pod/UR::Object::Property) - Metadata class for Properties [UR::Namespace](https://metacpan.org/pod/UR::Namespace) - Manage packages and classes [UR::Context](https://metacpan.org/pod/UR::Context) - Software transactions and More! [UR::DataSource](https://metacpan.org/pod/UR::DataSource) - How and where to get data # QUICK TUTORIAL First create a Namespace class for your application, Music.pm: package Music; use UR; class Music { is => 'UR::Namespace' }; 1; Next, define a data source representing your database, Music/DataSource/DB1.pm package Music::DataSource::DB1; use Music; class Music::DataSource::DB1 { is => ['UR::DataSource::MySQL', 'UR::Singleton'], has_constant => [ server => { value => 'database=music' }, owner => { value => 'music' }, login => { value => 'mysqluser' }, auth => { value => 'mysqlpasswd' }, ] }; or to get something going quickly, SQLite has smart defaults... class Music::DataSource::DB1 { is => ['UR::DataSource::SQLite', 'UR::Singleton'], }; Create a class to represent artists, who have many CDs, in Music/Artist.pm package Music::Artist; use Music; class Music::Artist { id_by => 'artist_id', has => [ name => { is => 'Text' }, cds => { is => 'Music::Cd', is_many => 1, reverse_as => 'artist' } ], data_source => 'Music::DataSource::DB1', table_name => 'ARTIST', }; Create a class to represent CDs, in Music/Cd.pm package Music::Cd; use Music; class Music::Cd { id_by => 'cd_id', has => [ artist => { is => 'Music::Artist', id_by => 'artist_id' }, title => { is => 'Text' }, year => { is => 'Integer' }, artist_name => { via => 'artist', to => 'name' }, ], data_source => 'Music::DataSource::DB1', table_name => 'CD', }; If the database does not exist, you can run this to generate the tables and columns from the classes you've written (very experimental): $ cd Music $ ur update schema If the database existed already, you could have done this to get it to write the last 2 classes for you: $ cd Music; $ ur update classes Regardless, if the classes and database tables are present, you can then use these classes in your application code: # Using the namespace enables auto-loading of modules upon first attempt to call a method use Music; # This would get back all Artist objects: my @all_artists = Music::Artist->get(); # After the above, further requests would be cached # if that set were large though, you might want to iterate gradually: my $artist_iter = Music::Artist->create_iterator(); # Get the first object off of the iterator my $first_artist = $artist_iter->next(); # Get all the CDs published in 2007 for the first artist my @cds_2007 = Music::Cd->get(year => 2007, artist => $first_artist); # Use non-equality operators: my @some_cds = Music::Cd->get( 'year between' => ['2004','2009'] ); # This will use a JOIN with the ARTISTS table internally to filter # the data in the database. @some_cds will contain Music::Cd objects. # As a side effect, related Artist objects will be loaded into the cache @some_cds = Music::Cd->get( year => '2007', 'artist_name like' => 'Bob%' ); # These values would be cached... my @artists_for_some_cds = map { $_->artist } @some_cds; # This will use a join to prefetch Artist objects related to the # objects that match the filter my @other_cds = Music::Cd->get( 'title like' => '%White%', -hints => ['artist'] ); my $other_artist_0 = $other_cds[0]->artist; # already loaded so no query # create() instantiates a new object in the current "context", but does not save # it in the database. It will autogenerate its own cd_id: my $new_cd = Music::Cd->create( title => 'Cool Album', year => 2009 ); # Assign it to an artist; fills in the artist_id field of $new_cd $first_artist->add_cd($new_cd); # Save all changes in the current transaction back to the database(s) # which are behind the changed objects. UR::Context->current->commit; # Environment Variables UR uses several environment variables to do things like run with database commits disabled, watching SQL queries run, examine query plans, and control cache size, etc. These make development and debugging fast and easy. See [UR::Env](https://metacpan.org/pod/UR::Env) for details. # DEPENDENCIES Class::Autouse Cwd Data::Dumper Date::Format DBI File::Basename FindBin FreezeThaw Path::Class Scalar::Util Sub::Installer Sub::Name Sys::Hostname Text::Diff Time::HiRes XML::Simple # AUTHORS UR was built by the software development team at the McDonnell Genome Institute at Washington University School of Medicine (Richard K. Wilson, PI). Incarnations of it run laboratory automation and analysis systems for high-throughput genomics. Anthony Brummett brummett@cpan.org Nathan Nutter Josh McMichael Eric Clark Ben Oberkfell Eddie Belter Feiyu Du Adam Dukes Brian Derickson Craig Pohl Gabe Sanderson Todd Hepler Jason Walker James Weible Indraniel Das Shin Leong Ken Swanson Scott Abbott Alice Diec William Schroeder Shawn Leonard Lynn Carmichael Amy Hawkins Michael Kiwala Kevin Crouse Mark Johnson Kyung Kim Jon Schindler Justin Lolofie Jerome Peirick Ryan Richt John Osborne Chris Harris Philip Kimmey Robert Long Travis Abbott Matthew Callaway James Eldred Scott Smith sakoht@cpan.org David Dooling # LICENCE AND COPYRIGHT Copyright (C) 2002-2015 Washington University in St. Louis, MO. This sofware is licensed under the same terms as Perl itself. See the LICENSE file in this distribution. ur100775023532023421 375512544604516 12773 0ustar00abrummetgsc000000000000UR-0.44/bin#!/usr/bin/env perl use strict; use warnings; BEGIN { if ($ENV{COMP_CWORD}) { eval "use Getopt::Complete::Cache class => 'UR::Namespace::Command', above => 1;"; exit if ($@); } if ($ENV{COMP_LINE}) { #for transitioning from older version of completion #just return no result exit; } }; use above "UR"; UR::Namespace::Command->execute_with_shell_params_and_exit(); =pod =head1 NAME B - command-line interface to UR =head1 DESCRIPTION The B command is the entry point for a suite of tools to create and manage a module tree of UR classes, data sources, and views. It also includes launchers for some built-in services. =head1 SUB-COMMANDS See the help on specific sub-commands for details. init NAMESPACE [DB] initialize a new UR app in one command define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging =head1 DEVELOPMENT =head2 PWD Running this WITHIN the source tree of a UR namespace will automatically "use lib" your tree. A message will appear to STDERR when this occurs. See the module for context-sensitive library usage info. =head2 MAC SOFTWARE MODULE API Looking for the docs on UR.pm on a Mac? Try "perldoc UR.pm" or "man UR". On some systems (Mac), perldoc will show this page for both "perldoc ur" and also "perldoc UR" due to filesystem case insensitivity. =head2 SOURCE UR is hosted on github, at: http://github.com/sakoht/ur =head1 BUGS Report bugs at http:://github.com/sakoht/ur/issues =head1 AUTHOR Scott Smith (sakoht) at cpan.org =cut UR.pm100664023532023421 127412544604516 14153 0ustar00abrummetgsc000000000000UR-0.44/builderpackage builder::UR; use warnings FATAL => 'all'; use strict; use parent 'Module::Build'; sub ACTION_build { my $self = shift; foreach my $metadb_type ( qw(sqlite3 sqlite3n sqlite3-dump sqlite3n-dump sqlite3-schema sqlite3n-schema) ) { $self->add_build_element($metadb_type); } return $self->SUPER::ACTION_build(@_); } sub ACTION_docs { # ensure docs get man pages and html my $self = shift; $self->depends_on('code'); $self->depends_on('manpages', 'html'); } sub man1page_name { # without this we have "man ur-init.pod" instead of "man ur-init" my ($self, $file) = @_; $file =~ s/.pod$//; return $self->SUPER::man1page_name($file); } 1; cpanfile100664023532023421 236712544604516 13351 0ustar00abrummetgsc000000000000UR-0.44requires 'perl', 'v5.8.7'; requires 'Carp'; requires 'Class::AutoloadCAN', '0.03'; requires 'Class::Autouse', '2.0'; requires 'Clone::PP', '1.02'; requires 'DBD::SQLite', '1.14'; requires 'DBI', '1.601'; requires 'Data::Compare', '0.13'; requires 'Data::UUID', '0.148'; requires 'Date::Format'; requires 'Devel::GlobalDestruction'; requires 'File::Basename', '2.73'; requires 'File::Path'; requires 'File::Temp'; requires 'FreezeThaw', '0.43'; requires 'Getopt::Complete', '0.26'; requires 'HTTP::Request'; requires 'JSON'; requires 'Lingua::EN::Inflect', '1.88'; requires 'List::MoreUtils'; requires 'MRO::Compat'; requires 'Path::Class'; requires 'Plack'; requires 'Pod::Simple::HTML', '3.03'; requires 'Pod::Simple::Text', '2.02'; requires 'Sub::Install', '0.924'; requires 'Sub::Name', '0.04'; requires 'Sys::Hostname', '1.11'; requires 'Template'; requires 'Text::Diff', '0.35'; requires 'Text::Glob'; requires 'YAML'; requires 'version'; requires 'Module::Runtime', 'v0.014'; on test => sub { requires 'Test::More', '0.98'; requires 'Test::Deep'; requires 'Test::Exception'; requires 'Test::Fatal'; requires 'Test::Fork'; }; on develop => sub { requires 'CPAN::Uploader'; requires 'Minilla', '1.1.0'; requires 'Version::Next'; }; README100664023532023421 16512544604516 14550 0ustar00abrummetgsc000000000000UR-0.44/dist-maintThese are manually run from the current directory to maintain parts of the distribution. They need work/replacement. findreplace100775023532023421 142112544604516 16106 0ustar00abrummetgsc000000000000UR-0.44/dist-maint#!/usr/bin/env perl use strict; use warnings; my $find = shift; my $replace = shift; my @files = @ARGV; unless (@files) { @files = ; } chomp(@files); for my $file (@files) { next if (-d $file); next if ($file =~ /.bak\d+/); print "Handling $file\n"; my $bak = $file . ".bak$$"; print `mv '$file' '$bak'`; print `grep -- '$find' '$bak'`; #print `cat $bak | sed 's/$find/$replace/g' > $file`; my $IN; my $OUT; open($IN,$bak) or warn "can't open $bak!" && next; open($OUT,">$file") or die; while (<$IN>) { $_ =~ s/$find/$replace/g; print $OUT $_; } close $OUT; close $IN; print `diff $bak $file`; print `rm $bak`; print "\n"; } #print "\\rm `find * | grep .bak$$\n"; update-pod.sh100775023532023421 20612544604516 16265 0ustar00abrummetgsc000000000000UR-0.44/dist-maint#!/usr/bin/env bash cd .. rm -rf pod cd lib/UR ur update pod -i .. -o ../../pod ur UR::Namespace::Command cd ../.. rm -rf pod/ur-old* update-ur-all.sh100775023532023421 36012544604516 16700 0ustar00abrummetgsc000000000000UR-0.44/dist-maint#!/usr/bin/env bash cat MANIFEST \ | grep ^lib \ | grep '\.pm$' \ | grep -v 'lib/above.pm' \ | grep -v 'lib/UR.pm' \ | grep -v 'lib/UR/All.pm' \ | perl -ne 'chomp; s|^lib/||; s|\.pm$||; s|/|::|g; print "use $_;\n";' update-version.pl100775023532023421 104612544604516 17214 0ustar00abrummetgsc000000000000UR-0.44/dist-maint#!/usr/bin/env perl use strict; use warnings; my $new = shift; die "please supply the new version number N.NN\n" unless $new; my $old = shift; $old ||= ($new - 0.01); print "updating version from $old to $new\n"; my $cmd = qq{cd ..; dist-maint/findreplace '$old"; # UR \\\$VERSION' '$new"; # UR \$VERSION' `grep -rn '# UR \\\$VERSION' lib/ | sed s/:.*//`}; print $cmd,"\n"; system $cmd; $cmd = qq{cd ..; dist-maint/findreplace ' version $old' ' version $new' `grep -rn 'This document describes ' lib/ | sed s/:.*//`}; print $cmd,"\n"; system $cmd; common.yml100664023532023421 63312544604516 15174 0ustar00abrummetgsc000000000000UR-0.44/gmt-web--- layout: sub menu: - text: Overview href: index.html - text: Documentation href: documentation.html - text: Install href: install.html module_details: name: 'UR' exe_name: ur gmt_pkg_name: ur debian_pkg_name: libur-perl icon: res/images/icon_48.png icon_16: res/images/icon_16.png download_link: install.html github_url: https://github.com/genome/UR documentation.html100664023532023421 2735412544604516 20443 0ustar00abrummetgsc000000000000UR-0.44/gmt-web/content

UR User Manual



NAME

UR::Manual::Overview - UR from Ten Thousand Feet


Perspective on Objects

Standard software languages provide a facility for making objects. Those objects have certain characteristics which are different with UR objects.

A standard object in most languages:

  • exists only as long as the program which created it has a reference to it
  • requires that the developer manage organizing the object(s) into a structure to support any searching required
  • handles persistence between processes explicitly, by saving or loading the object to external storage
  • references other objects only if explicitly linked to those objects
  • acts as a functional software device, but any meaning associated with the object is implied by how it is used

Regular objects like those described above are the building blocks of most software.

In many cases, however, they are often used for a second, higher-level purpose: defining entities in the domain model of the problem area the software addresses. UR objects are tailored to represent domain model entities well. In some sense, UR objects follow many of the design principles present in relational databases, and as such mapping to a database for UR objects is trivial, and can be done in complex ways.

UR objects differ from a standard object in the following key ways:

  • the object exists after creation until explicitly deleted, or the transaction it is in rolled-back
  • managing loaded objects is done automatically by a Context object, which handles queries, saving, lazy-loading and caching
  • it is possible to query for an object by specifying the class and the matching characteristics
  • the object can reference other objects which are not loaded in the current process, and be referenced by objects not in the current process
  • the object is a particular truth-assertion in the context in which it exists

Object-Relational Mapping

UR's primary reason for existing is to function as an ORM. That is, managing how to store instances of objects in memory of a running program with more persistant storage in a relational database, and retrieve them later. It handles the common cases where each table is implemented by a class their columns are properties of the classes; retrieving objects by arbitrary properties; creating, updating and deleting objects with enforced database constraints; and named relationships between classes.

It can also handle more complicated things like:

  • classes for things which are not database entities at all
  • derived classes where the data spans multiple tables between the parent and child classes
  • loading an object through a parent class and having it automatically reblessed into the appropriate subclass
  • properties with no DB column behind them
  • calculated properties with a formula behind them
  • inheritance hierarchies that may have tables missing at some or all stages
  • meta-data about Properties, Classes and the relationships between them

Object Context

With UR, every object you create is made a part of the current ``Context''. Conceptually, the Context is the lens by which your application views the data that exists in the world. At one level, you can think of the current context as an in-memory transaction. All changes to the object are tracked by the context. The Context knows how to map objects to their storage locations, called Data Sources. Saving your changes is simply a matter of asking the current context to commit.

The Context can also reverse the saving process, and map a request for an object to a query of external storage. Requests for objects go through the Context, are loaded from outside as needed, and are returned to the caller after being made part of the current context's transaction.

Objects never reference each other by actual Perl reference internally, instead they use the referent's ID. Accessors on an object which return another object send the ID through the context to get the object back, allowing the context to load the referenced object only when it is actually needed. This means that your objects can hook together until references span an entire database schema, and pulling one object from the database will not load the entire database into memory.

The context handles caching, and by default will cache everything it touches. This means that you can ask for the same thing multiple times, and only the first request will actually hit the underlying database. It also means that requests for objects which map to the same ID will return the exact same instance of the object.

The net effect is that each process's context is an in-memory database. All object creation, deletion, and change is occurring directly to that database. For objects configured to have external persistence, this database manages itself as a ``diff'' vs. the external database, allowing it to simulate representing all UR data everywhere, while only actually tracking what is needed.

Benefits

  • database queries don't repeat themselves again and again
  • you never write insert/update/delete statements, or work out constraint order yourself
  • allows you to write methods which address an object individually, with ways to avoid many individual database queries
  • explicitly clearing the cache is less complex than explicitly managing the caching of data

Issues

  • the cache grows until you explicitly clear it, or allow the Context to prune the cache by setting object count limits explicitly
  • there is CPU overhead checking the cache if you really are always going directly to the database

Class Definitions

At the top of every module implementing a UR class is a block of code that defines the class to explicitly spell out its inheritance, properties and types, constraints, relationships to other classes and where the persistent storage is located. It's meant to be easy to read and edit, if necessary. If the class is backed by a database table, then it can also maintain itself.


Metadata

Besides the object instances representing data used by the program, the UR system has other objects representing metadata about the classes (class information, properties, relationships, etc), database entities (databases, tables, columns, constraints, etc), transactions, data sources, etc. All the metadata is accessable through the same API as any of the database-backed data.

For classes backed by the database, after a schema change (like adding tables or columns, altering types or constraints), a command-line tool can automatically detect the change and alter the class definition in the Perl module to keep the metadata in sync with the database.


Documentation System

At the simplest level, most entities have a 'doc' metadata attribute to attach some kind of documentation to. There's also a set of tools that can be run from the command line or a web browser to view the documentation. It can also be used to browse through the class and database metadata, and generate diagrams about the metadata.


Iterators

If a retrieval from the database is likely to result in the generation of tons of objects, you can choose to get them back in a list and keep them all in memory, or get back a special Iterator object that the program can use to get back objects in batches.


Command Line Tools

UR has a central command-line tool that cam be used to manipulate the metadata in different ways. Setting up namespaces, creating data sources, syncing classes with schemas, accessing documentation, etc.

There is also a framework for creating classes that represent command line tools, their parameters and results, and makes it easy to create tools through the Command Pattern.


Example

Given these classes:

PathThing/Path.pm

  use strict;
  use warnings;

  use PathThing;  # The application's UR::Namespace module

  class PathThing::Path {
      id_by => 'path_id',
      has => [
          desc   => { is => 'String' },
          length => { is => 'Integer' },
      ],
      data_source => 'PathThing::DataSource::TheDB',
      table_name => 'PATHS',
  };

PathThing/Node.pm

  class PathThing::Node {
      id_by => 'node_id',
      has => [
          left_path => { is => 'PathThing::Path', id_by => 'left_path_id' },
          left_path_desc => { via => 'left_path', to => 'desc' },
          left_path_length => { via => 'left_path', to => 'length' },
          right_path => { is => 'PathThing::Path', id_by => 'right_path_id' },
          right_path_desc => { via => 'right_path', to => 'desc' },
          right_path_length => { via => 'right_path', to => 'length' },
          map_coord_x => { is => 'Integer' },
          map_coord_y => { is => 'String' },
      ],
      data_source => 'PathThing::DataSource::TheDB',
      table_name => 'NODES',
  };

For a script like this one:

  use PathThing::Node;
  my @results = PathThing::Node->get(
                    right_path_desc => 'over the river',
                    left_path_desc => 'through the woods',
                    right_path_length => 10,
                );

It will generate SQL like this:

  select NODES.NODE_ID, NODES.LEFT_PATH_ID, NODES.RIGHT_PATH_ID,
         NODES.MAP_COORD_X, NODES.MAP_COORD_Y,
         left_path_1.PATH_ID, left_path_1.DESC, left_path_1.LENGTH
         right_path_1.PATH_ID, right_path_1.DESC, right_path_1.LENGTH
  from NODES
  join PATHS left_path_1 on NODES.LEFT_PATH_ID = left_path_1.PATH_ID
  join PATHS right_path_1 on NODES.RIGHT_PATH_ID = right_path1.PATH_ID
  where left_path_1.DESC = 'through the woods'
    and right_path_1.DESC = 'over the river',
    and right_path_1.LENGTH = 10

And for every row returned by the query, a PathThing::Node and two PathThing::Path objects will be instantiated and stored in the Context's cache. @results will contain a list of matching PathThing::Node objects.

index.html100664023532023421 271612544604516 16654 0ustar00abrummetgsc000000000000UR-0.44/gmt-web/content

Introduction


UR is a Class Framework and Object/Relational Mapper (ORM) for Perl.

After installing, run the "ur" command for a list of options.

As a Class Framework, it starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and builds upon that with a more formal way to describe classes and their properties, object caching, and metadata about the classes and the ways they connect to each other.

As an ORM, it aims to relieve the developer from having to think about the SQL behind any particular request, instead using the class structure and its metadata as a guide for where the data will be found. Behind the scenes, the RDBMS portion can handle JOINs (both INNER and OUTER) representing inheritance and indirect properties, multi-column primary and foreign keys, and iterators. It does its best to only query the database for information you've directly asked for, and to not query the database for something that has been loaded before. Oracle, SQLite, MySQL and PostgreSQL are all supported.

Additionally, UR can use files or collections of files as if they were tables in a database, as well as internally handling the equivalent of an SQL join between two or more databases if that's what the query and class structure indicates.

UR.pm contains more introductory POD documentation. UR::Manual has a short list of documentation you're likely to want to see next.

install.md100664023532023421 33512544604516 16622 0ustar00abrummetgsc000000000000UR-0.44/gmt-web/content{% include install/download.html %} It is also availabe from [CPAN](http://search.cpan.org/search?mode=all&query=UR). {% include install/github.html %} {% include install/help.html %} {% include install/manuals.html %} icon_16.png100664023532023421 127412544604516 20657 0ustar00abrummetgsc000000000000UR-0.44/gmt-web/content/res/images‰PNG  IHDR‘h6tEXtSoftwareAdobe ImageReadyqÉe<^IDATxÚdRAOAÞ™Ù]¶»•VÛhL[MÅH0èÅVƒ!ñ` iLüpóÀðÀÅ 'âÕp=6)7Â¥mê¦!F!à¡`imKÙÝñ›Ý£“ÙÙyoÞ7ïûÞ²½ýñðð@ùo!|ßçÜó}SUc´R©¨WWÃR©L¡”à›pEç<5‘-<}vtôcˆN篢Ãh!äÄV¢‰ð¸òж''ïõz—ºnø¾ƒœ4`"hd29ÓÅAWÆÆ&4MK&“ñx’sßq< д€ %‹Ï_¬®¾[[[O¥²ŽF^*uÓ4ÍlöV>?ϘfY×X±øªÙ<HÛÎçrw±Ÿ}b‘BaqfƆ‰™LveåÍÉÉ/5Ì 5SZ*}]XXŠÇâÜãÅâ[x@2\Ã`iÑÊ9wvv>é#ºa¸^Çqz½Þ`0ÀbP=ú„ð<øžÚívqÛuÝV«Õn·v»,˲íG­­H›Nßît.`NO?H${zzÚl6Êå/ƒ~G! <œ+Ã¡Š bnn~|<%ùQöÎÏÏÊåÏŒù×oŒÂéºür0l4š J\W–P×õ°Í€õûýF£ŠÛ¢Q÷<¡i~Ù1ü66ÖÓé;Ë˯mû1A‹‚$mÓ„øظ^¦i²W”1bYæYëxsó}½^c^jÈa,l(b䟄ñi¯#µh0b±ØÔÔ}èDå­ÁŠ|‘êÞÞn½Þ4ŒèÒÒËZí[µZA™ÛíßÕÊwtLmpâ-¹ÿ(6&Ë$ñ;£IEND®B`‚icon_48.png100664023532023421 543012544604516 20662 0ustar00abrummetgsc000000000000UR-0.44/gmt-web/content/res/images‰PNG  IHDR00Ø`nÐtEXtSoftwareAdobe ImageReadyqÉe< ºIDATxÚ¬YkoTÇ>3ç¬wïÆÆcǦ$(¡uqŒk7\$¨BÓÒ JÕP ä&"Iå2ZUíçü¤6R$ÚðÅJZQL X ‚« upk Q‚ñ³öî™é3óÎÌÎ.kC¥ ËÙsöÌå™÷ú¼cvò䇇ÿ|Mª¦¾é^ˆ¼Gêã+¾}™ßÔ×åË—£……ô©S§M'Ù¬.ÞýãþfQßÒ¶À@#49XÞ è gfæ£àkj 42¡ï¥•Ð88c’ðXpÒÍ£qÎ-ð1X©±€È#ï k³@ˆ@¯§nðcYYy*µ¢´tEyyiMMíâââÈÈ—ÓÓs•’7ÊðEš'T †ך™N 5Iï_}õІ ›²ÙlÇ¢˜žžžœš›šzà-!ýíEO´SßJò„ëɲ¨Ö„ î2™ŒÐ o§¦¦ª«ëÉÎ4$A[ÓŠ Œ„ T²å±èo_,À¼X®ûÁœYÔÕ­‚=`m|Èr£(*[VñèѬ œQ­•1ßbè›¶kod–XîQk‘¨/a&Ò+*ª`4˜ŸóÖ«¨¨¨¬¬¬®^9??§!¨qZË$[O—҉_è}«]Ʊ’<®±þÒOÂ:õÄHý›É’äXܶ0 Ë`äåå+WÖIA³=­Ôó«ÆÉËlcî†j"½WžçIÆP´çïÁ,à¤ÙòŠÊH·Õø ¥R)jjjI”¤„±G#¡¶"}@,ÿ†4,!1¬©©¹·÷­òòJg½¹ ê®®¤ÂC` CõA&h bªª® ò§±zæË:£/aä çíìì:rä7ë×oxíµw[ð‹öh¥=}5jÔ7²¬¼‚"¢`©›ªª*`ª­­£!Y|²4P]HBÌwâqwdéû÷8xð-’hMMMoï/;:¾_6†D¸sAYÖÖT:G¶%ôµ´´v½®íYmšÂì[W¯]¤¶K»è"H +‹üüÝqB»5Œc_ye_[Û³~x Ñù¦µrY[[O")ˆjÐZuuuc㚺ºúññ¯ K: ,nÚ´9Ü»÷CC7œ·@’ü‘2€d¤ÍÍ-}}¿~ï½ãŸ}vÄ€äÕÞÞ¹yó–P7?‘¢0J&Kššš:;»éíøø}ÈìêÕ! (Ô*ËFzR¢×à‚ÁÁO¶o¹¡¡h³AÃb±!ô¾ýö‘3=ÿ³»wïÙµëûx…žxE{°ƒ¬……fQg  «ÑÙDêeš¦O¼áĉ?=ú[Œ¤”ä7¬MÂûáž}{÷ý äkdIbnÎ।Q¹à+ˆ™0(4ôèÌ&|µ-Á”æÐéÖ­áK—Îwwï0ç1@ʵL†9»ñé¢M6—æ H!9RÎÄÊÈ(!Ù˜a›ýýtv¾”L&…%§~ã–ÄhæÁ`·£`yæ³^'µüd^0Ò£À&S‚Éô÷ÿÉ…ðeןÿ³¹Ìà$ÌimòL‚Ü^*GUO»wï.ùüS¬°\Ÿ”ú?r‘\„ÀÇY>‘$îåjtŽßÿ¸‡Š¶¢'¶ü.Ne†d8f.,¯&‘¹Â!¸~ýêààù¼…Ÿ¢a6ðÆôBzaa10_Q¹)®§ ØT"Ò¸:1®\äȉ7¸páܶm;É·—%¾R ¤Ó ‡"²RHòÊʪÙÙD)&Ó§—…ã›7¯Д”„Q"$4Šl1GQLxsGB2“*HT œãر76oîÙ²eëš5ͰPª+žFk˜ ‚ !¨pà BSH‰@Ñ ÌªÃ5D“щb¨%Ãx]|ùòù‹Ï>÷܆­[_†ÓAý‹û%ÒcÈ(‚CFóó3©TI¨5Eÿì1ƒfË}œî¥â…6†QP"¦  ŒT,•ðŸÝ¾=‚²¦ºzå®ïýøÛí/¼×I‰EQ’K§…º4Õõ³a,Z¡MÃÌ£ÈÜI ºuÌÅ´TR¦© Ó³èÊ.ÁDÓÓ>øóñcÇ~ÕßâîÝ;EC€(޳ ¸qr=8{òù¤?1'f¨xB™s˜Ô^›Ð Aâôé¿ôõ½D½²t"Áµg³KsväªDNì6ÇáxÅTÑ€z3&]Q– C£L&»”„¨^‘ Á¸°iË¢Ic* Ñ+éŽ1ªbè 1òÍÂVt±="âäŠxYJàT.VUU»s‘; tþ’Kuõ‹å\#ùþápèónºNMM,Iæ9á*/¯XU¿Ú„¯HÅþµœ ]±‡Á¾Ê¤åªêÊMy˜1tÐàªIƒšóTªtÊ 7-éùî.{þGʯÃÔ¼zÛÌÆéj1êLWÊejtÓ‡MÂRD… aéðá#MMÍô¢¨%ÑYݺu0c&«Pòl¬‹x:½×¨b]7xš¢ò_ºKªí¬èäÖž=ªŽxsèÐ;;vì„ÕZùÓ+Hö0#uMMmOÏÎÁÁ„8̾±­ƒ¥Þ°ÿ÷ˆŽ 6Ç7"K‡ Tߨ?PÈ8«ÎØð~íÚÖÇûÖ®mñÍ;Ò§†˜­mÛ¶ûÚµKéôCò}¢ “«H›èðägcs"¨Ãd48øñ_ÚLäÚÛ_liÙ40ðq~ùý„†t6??ÏXÍ•+Ÿj­…Þé`®$w–E£q‡‚XõÚÿÆ mï¾õdÏIEND®B`‚Command.pm100664023532023421 32312544604516 14275 0ustar00abrummetgsc000000000000UR-0.44/libpackage Command; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is_abstract => 1, subclassify_by_version => 1, ); 1; Common.pm100664023532023421 1033212544604516 15566 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::Common; use strict; use warnings; use UR; # Once roles exist this should probably be a role. class Command::Common { is_abstract => 1, valid_signals => [qw(error_die)], }; sub create { my $class = shift; my ($rule,%extra) = $class->define_boolexpr(@_); my @params_list = $rule->params_list; my $self = $class->SUPER::create(@params_list, %extra); return unless $self; # set non-optional boolean flags to false. # TODO: rename that property meta method if it is not ONLY used for shell args for my $property_meta ($self->_shell_args_property_meta) { my $property_name = $property_meta->property_name; if (!$property_meta->is_optional and !defined($self->$property_name)) { if (defined $property_meta->data_type and $property_meta->data_type =~ /Boolean/i) { $self->$property_name(0); } } } return $self; } sub shortcut { my $self = shift; return unless $self->can('_shortcut_body'); my $result = $self->_shortcut_body; $self->result($result); return $result; } sub execute { # This is a wrapper for real execute() calls. # All execute() methods are turned into _execute_body at class init, # so this will get direct control when execute() is called. my $self = shift; #TODO handle calls to SUPER::execute() from another execute(). # handle calls as a class method my $was_called_as_class_method = 0; if (ref($self)) { if ($self->is_executed) { Carp::confess("Attempt to re-execute an already executed command."); } } else { # called as class method # auto-create an instance and execute it $self = $self->create(@_); return unless $self; $was_called_as_class_method = 1; } # handle __errors__ objects before execute if (my @problems = $self->__errors__) { for my $problem (@problems) { my @properties = $problem->properties; $self->error_message("Property " . join(',', map { "'$_'" } @properties) . ': ' . $problem->desc); } my $command_name = $self->command_name; $self->error_message("Please see '$command_name --help' for more information."); $self->delete() if $was_called_as_class_method; return; } my $result = eval { $self->_execute_body(@_); }; my $error = $@; if ($error or not $result) { my %error_data; $error_data{die_message} = defined($error) ? $error:''; $error_data{error_message} = defined($self->error_message) ? $self->error_message:''; $error_data{error_package} = defined($self->error_package) ? $self->error_package:''; $error_data{error_file} = defined($self->error_file) ? $self->error_file:''; $error_data{error_subroutine} = defined($self->error_subroutine) ? $self->error_subroutine:''; $error_data{error_line} = defined($self->error_line) ? $self->error_line:''; $self->__signal_observers__('error_die', %error_data); die $error if $error; } $self->is_executed(1); $self->result($result); return $self if $was_called_as_class_method; return $result; } sub _execute_body { # default implementation in the base class # Override "execute" or "_execute_body" to implement the body of the command. # See above for details of internal implementation. my $self = shift; my $class = ref($self) || $self; if ($class eq __PACKAGE__) { die "The execute() method is not defined for $_[0]!"; } return 1; } # Translates a true/false value from the command module's execute() # from Perl (where positive means success), to shell (where 0 means success) # Also, execute() could return a negative value; this is converted to # positive and used as the shell exit code. NOTE: This means execute() # returning 0 and -1 mean the same thing sub exit_code_for_return_value { my $self = shift; my $return_value = shift; if (! $return_value) { $return_value = 1; } elsif ($return_value < 0) { $return_value = 0 - $return_value; } else { $return_value = 0 } return $return_value; } Shell.pm100664023532023421 12454312544604516 17176 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Dispatchpackage Command::V2; # additional methods to dispatch from a command-line use strict; use warnings; use IO::File; use List::MoreUtils; # instead of tacking these methods onto general Command::V2 objects # they could be put on the Command::Shell class, which is a wrapper/adaptor Command for translating from # command-line shell to purely functional commands. # old entry point # new cmds will call Command::Shell->run("MyClass",@ARGV) # which goes straight into _cmdline_run for now... sub execute_with_shell_params_and_exit { my $class = shift; if (@_) { die "No params expected for execute_with_shell_params_and_exit()!"; } my @argv = @ARGV; @ARGV = (); my $exit_code = $class->_cmdline_run(@argv); exit $exit_code; } sub _cmdline_run { # This automatically parses command-line options and "does the right thing": # TODO: abstract out all dispatchers for commands into a given API my $class = shift; my @argv = @_; $Command::entry_point_class ||= $class; $Command::entry_point_bin ||= File::Basename::basename($0); if ($ENV{COMP_CWORD}) { require Getopt::Complete; my @spec = $class->resolve_option_completion_spec(); my $options = Getopt::Complete::Options->new(@spec); $options->handle_shell_completion; die "error: failed to exit after handling shell completion!"; } my $exit_code; eval { $exit_code = $class->_execute_with_shell_params_and_return_exit_code(@argv); UR::Context->commit or die "Failed to commit!: " . UR::Context->error_message(); }; if ($@) { $class->error_message($@); UR::Context->rollback or die "Failed to rollback changes after failed commit!!!\n"; $exit_code = 255 unless ($exit_code); } return $exit_code; } sub _execute_with_shell_params_and_return_exit_code { my $class = shift; my @argv = @_; my $original_cmdline = join("\0",$0,@argv); # make --foo=bar equivalent to --foo bar @argv = map { ($_ =~ /^(--\w+?)\=(.*)/) ? ($1,$2) : ($_) } @argv; my ($delegate_class, $params, $errors) = $class->resolve_class_and_params_for_argv(@argv); my $exit_code; if ($errors and @$errors) { $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); for my $error (@$errors) { $delegate_class->error_message(join(' ', $error->property_names) . ": " . $error->desc); } $exit_code = 1; } else { my $rv = $class->_execute_delegate_class_with_params($delegate_class,$params,$original_cmdline); $exit_code = $delegate_class->exit_code_for_return_value($rv); } return $exit_code; } sub _execute_delegate_class_with_params { # this is called by both the shell dispatcher and http dispatcher for now my ($class, $delegate_class, $params, $original_cmdline) = @_; unless ($delegate_class) { $class->dump_status_messages(1); $class->dump_warning_messages(1); $class->dump_error_messages(1); $class->dump_usage_messages(1); $class->dump_debug_messages(0); $class->usage_message($class->help_usage_complete_text); return; } $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); $delegate_class->dump_usage_messages(1); $delegate_class->dump_debug_messages(0); # FIXME There should be a better check for params that are there because they came from the # command line, and params that exist for infrastructural purposes. 'original_command_line' # won't ever be given on the command line and shouldn't count toward the next test. # maybe check the is_input properties... if ( !defined($params) ) { my $command_name = $delegate_class->command_name; $delegate_class->status_message($delegate_class->help_usage_complete_text); $delegate_class->error_message("Please specify valid params for '$command_name'."); return; } if ( $params->{help} ) { $delegate_class->usage_message($delegate_class->help_usage_complete_text); return 1; } $params->{'original_command_line'} = $original_cmdline if (defined $original_cmdline); my $command_object = $delegate_class->create(%$params); unless ($command_object) { # The delegate class should have emitted an error message. # This is just in case the developer is sloppy, and the user will think the task did not fail. print STDERR "Exiting.\n"; return; } $command_object->dump_status_messages(1); $command_object->dump_warning_messages(1); $command_object->dump_error_messages(1); $command_object->dump_debug_messages($command_object->debug); if ($command_object->debug) { UR::ModuleBase->dump_debug_messages($command_object->debug); } my $rv = $command_object->execute($params); if ($command_object->__errors__) { $command_object->delete; } return $rv; } sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; my ($params_hash,@spec) = $self->_shell_args_getopt_specification; unless (grep { /^help\W/ } @spec) { push @spec, "help!"; } my @error_tags; # Thes nasty GetOptions modules insist on working on # the real @ARGV, while we like a little more flexibility. # Not a problem in Perl. :) (which is probably why it was never fixed) local @ARGV; @ARGV = @argv; do { # GetOptions also likes to emit warnings instead of return a list of errors :( my @errors; my $rv; { local $SIG{__WARN__} = sub { push @errors, @_ }; ## Change the pattern to be '--', '-' followed by a non-digit, or '+'. ## This s the effect of treating a negative number as a value of an option. ## This means that we won't be allowed to have an option named, say, -1. ## But since command modules' properties have to be allowable function names, ## and "1" is not a valid function name, it's not really a problem #Getopt::Long::Configure('prefix_pattern=--|-(?!\D)|\+'); $rv = GetOptions($params_hash,@spec); } unless ($rv) { for my $error (@errors) { $self->error_message($error); } return($self, undef); } }; # Q: Is there a standard getopt spec for capturing non-option paramters? # Perhaps that's not getting "options" :) # A: Yes. Use '<>'. But we need to process this anyway, so it won't help us. if (my @names = $self->_bare_shell_argument_names) { for (my $n=0; $n < @ARGV; $n++) { my $name = $names[$n]; unless ($name) { $self->error_message("Unexpected bare arguments: @ARGV[$n..$#ARGV]!"); return($self, undef); } my $value = $ARGV[$n]; my $meta = $self->__meta__->property_meta_for_name($name); if ($meta->is_many and $n == $#names) { # slurp the rest $params_hash->{$name} = [@ARGV[$n..$#ARGV]]; last; } else { $params_hash->{$name} = $value; } } } if (@ARGV and not $self->_bare_shell_argument_names) { ## argv but no names $self->error_message("Unexpected bare arguments: @ARGV!"); return($self, undef); } for my $key (keys %$params_hash) { # handle any has-many comma-sep values my $value = $params_hash->{$key}; if (ref($value)) { my @new_value; for my $v (@$value) { my @parts = split(/,\s*/,$v); push @new_value, @parts; } @$value = @new_value; } elsif ($value eq q('') or $value eq q("")) { # Handle the special values '' and "" to mean undef/NULL $params_hash->{$key} = ''; } # turn dashes into underscores my $new_key = $key; next unless ($new_key =~ tr/-/_/); if (exists $params_hash->{$new_key} && exists $params_hash->{$key}) { # this corrects a problem where is_many properties badly interact # with bare args leaving two entries in the hash like: # a-bare-opt => [], a_bare_opt => ['with','vals'] delete $params_hash->{$key}; next; } $params_hash->{$new_key} = delete $params_hash->{$key}; } # futher work is looking for errors, and may display them # if help is set, return now # we might have returned sooner, but having full info available # allows for dynamic help if ($params_hash->{help}) { return ($self, $params_hash); } ## my $params = $params_hash; my $class = $self->class; if (my @errors = $self->_errors_from_missing_parameters($params)) { return ($class, $params, \@errors); } unless (@_) { return ($class, $params); } # should this be moved up into the methods which are only called # directly from the shell, or is it okay everywhere in this module to # presume we're a direct cmdline call? -ssmith local $ENV{UR_COMMAND_DUMP_STATUS_MESSAGES} = (!exists($ENV{UR_COMMAND_DUMP_STATUS_MESSAGES}) or $ENV{UR_COMMAND_DUMP_STATUS_MESSAGES}); my @params_to_resolve = $self->_params_to_resolve($params); for my $p (@params_to_resolve) { my $param_arg_str = join(',', @{$p->{value}}); my $pmeta = $self->__meta__->property($p->{name}); my @params; eval { @params = $self->resolve_param_value_from_cmdline_text($p); }; if ($@) { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$p->{name}], desc => "Errors while resolving from $param_arg_str: $@", ); } if (@params and $params[0]) { if ($pmeta->{'is_many'}) { $params->{$p->{name}} = \@params; } else { $params->{$p->{name}} = $params[0]; } } else { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$p->{name}], desc => "Problem resolving from $param_arg_str.", ); } } if (@error_tags) { return ($class, undef, \@error_tags); } else { return ($class, $params); } } sub resolve_option_completion_spec { my $class = shift; my @completion_spec = $class->_shell_args_getopt_complete_specification; no warnings; unless (grep { /^help\W/ } @completion_spec) { push @completion_spec, "help!" => undef; } return \@completion_spec } sub _errors_from_missing_parameters { my ($self, $params) = @_; my $class_meta = $self->__meta__; my @all_property_metas = $class_meta->properties(); my @specified_property_metas = grep { exists $params->{$_->property_name} } @all_property_metas; my %specified_property_metas = map { $_->property_name => $_ } @specified_property_metas; my %set_indirectly; my @todo = @specified_property_metas; while (my $property_meta = shift @todo) { if (my $via = $property_meta->via) { if (not $property_meta->is_mutable) { my $list = $set_indirectly{$via} ||= []; push @$list, $property_meta; } unless ($specified_property_metas{$via}) { my $via_meta = $specified_property_metas{$via} = $class_meta->property($via); push @specified_property_metas, $via_meta; push @todo, $via_meta; } } elsif (my $id_by = $property_meta) { my $list = $set_indirectly{$id_by} ||= []; push @$list, $property_meta; unless ($specified_property_metas{$id_by}) { my $id_by_meta = $specified_property_metas{$id_by} = $class_meta->property($id_by); push @specified_property_metas, $id_by_meta; push @todo, $id_by_meta; } } } # TODO: this should use @all_property_metas, and filter down to is_param and is_input # This old code just ignores things inherited from a base class. # We will need to be careful fixing this because it could add checks to tools which # work currently and lead to unexpected failures. my @property_names; if (my $has = $class_meta->{has}) { @property_names = List::MoreUtils::uniq(keys %$has); } my @property_metas = map { $class_meta->property_meta_for_name($_); } @property_names; my @error_tags; for my $property_meta (@property_metas) { my $pn = $property_meta->property_name; next if $property_meta->is_optional; next if $property_meta->implied_by; next if defined $property_meta->default_value; next if defined $params->{$pn}; next if $set_indirectly{$pn}; if (my $via = $property_meta->via) { if ($params->{$via} or $set_indirectly{$via}) { next; } } my $arg = $pn; $arg =~ s/_/-/g; $arg = "--$arg"; if ($property_meta->is_output and not $property_meta->is_input and not $property_meta->is_param) { if ($property_meta->_data_type_as_class_name->__meta__->data_source and not $property_meta->_data_type_as_class_name->isa("UR::Value") ) { # outputs with a data source do not need a specification # on the cmdline to "store" them after execution next; } elsif ($property_meta->is_calculated) { # outputs that are calculated don't need to be specified on # the command line next; } else { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$pn], desc => "Output requires specified destination: " . $arg . "." ); } } else { $DB::single = 1; push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$pn], desc => "Missing required parameter: " . $arg . "." ); } } return @error_tags; } sub _params_to_resolve { my ($self, $params) = @_; my @params_to_resolve; if ($params) { my $cmeta = $self->__meta__; my @params_will_require_verification; my @params_may_require_verification; for my $param_name (keys %$params) { my $pmeta = $cmeta->property($param_name); unless ($pmeta) { # This message was a die after a next, so I guess it isn't supposed to be fatal? $self->warning_message("No metadata for property '$param_name'"); next; } my $param_type = $pmeta->data_type; next unless($self->_can_resolve_type($param_type)); my $param_arg = $params->{$param_name}; if (my $arg_type = ref($param_arg)) { next if $arg_type eq $param_type; # param is already the right type if ($arg_type ne 'ARRAY') { $self->error_message("no handler for property '$param_name' with argument type " . ref($param_arg)); next; } } else { $param_arg = [$param_arg]; } next unless (@$param_arg); my $resolve_info = { name => $param_name, class => $param_type, value => $param_arg, }; push(@params_to_resolve, $resolve_info); my $require_user_verify = $pmeta->{'require_user_verify'}; if ( defined($require_user_verify) ) { push @params_will_require_verification, "'$param_name'" if ($require_user_verify); } else { push @params_may_require_verification, "'$param_name'"; } } my @adverbs = ('will', 'may'); my @params_adverb_require_verification = ( \@params_will_require_verification, \@params_may_require_verification, ); for (my $i = 0; $i < @adverbs; $i++) { my $adverb = $adverbs[$i]; my @param_adverb_require_verification = @{$params_adverb_require_verification[$i]}; next unless (@param_adverb_require_verification); if (@param_adverb_require_verification > 1) { $param_adverb_require_verification[-1] = 'and ' . $param_adverb_require_verification[-1]; } my $param_str = join(', ', @param_adverb_require_verification); $self->status_message($param_str . " $adverb require verification..."); } } return @params_to_resolve; } sub _can_resolve_type { my ($self, $type) = @_; return 0 unless($type); my $non_classes = 0; if (ref($type) ne 'ARRAY') { $non_classes = $type !~ m/::/; } else { $non_classes = scalar grep { ! m/::/ } @$type; } return $non_classes == 0; } sub _shell_args_property_meta { my $self = shift; my $class_meta = $self->__meta__; # Find which property metas match the rules. We have to do it this way # because just calling 'get_all_property_metas()' will product multiple matches # if a property is overridden in a child class my ($rule, %extra) = UR::Object::Property->define_boolexpr(@_); my %seen; my (@positional,@required_input,@required_param,@optional_input,@optional_param, @output); my @property_meta = $class_meta->properties(); PROP: foreach my $property_meta (@property_meta) { my $property_name = $property_meta->property_name; next if $seen{$property_name}++; next unless $rule->evaluate($property_meta); next unless $property_meta->can("is_param") and ($property_meta->is_param or $property_meta->is_input or $property_meta->is_output); if (%extra) { no warnings; for my $key (keys %extra) { if ($property_meta->$key ne $extra{$key}) { next PROP; } } } next if $property_name eq 'id'; next if $property_name eq 'result'; next if $property_name eq 'is_executed'; next if $property_name eq 'original_command_line'; next if $property_name =~ /^_/; next if $property_meta->implied_by; next if $property_meta->is_calculated; # Kept commented out from UR's Command.pm, I believe is_output is a workflow property # and not something we need to exclude (counter to the old comment below). #next if $property_meta->{is_output}; # TODO: This was breaking the G::M::T::Annotate::TranscriptVariants annotator. This should probably still be here but temporarily roll back next if $property_meta->is_transient; next if $property_meta->is_constant; if (($property_meta->is_delegated) || (defined($property_meta->data_type) and $property_meta->data_type =~ /::/)) { next unless($self->can('resolve_param_value_from_cmdline_text')); } else { next unless($property_meta->is_mutable); } if ($property_meta->{shell_args_position}) { push @positional, $property_meta; } elsif ($property_meta->is_optional) { if ($property_meta->is_input or $property_meta->is_output) { push @optional_input, $property_meta; } elsif ($property_meta->is_param) { push @optional_param, $property_meta; } } else { if ($property_meta->is_input or $property_meta->is_output) { push @required_input, $property_meta; } elsif ($property_meta->is_param) { push @required_param, $property_meta; } } } my @result; @result = ( (sort { $a->position_in_module_header cmp $b->position_in_module_header } @required_param), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @optional_param), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @required_input), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @optional_input), (sort { $a->shell_args_position <=> $b->shell_args_position } @positional), ); return @result; } sub _shell_arg_name_from_property_meta { my ($self, $property_meta,$singularize) = @_; my $property_name = ($singularize ? $property_meta->singular_name : $property_meta->property_name); my $param_name = $property_name; $param_name =~ s/_/-/g; return $param_name; } sub _shell_arg_getopt_qualifier_from_property_meta { my ($self, $property_meta) = @_; my $many = ($property_meta->is_many ? '@' : ''); if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { return '!' . $many; } #elsif ($property_meta->is_optional) { # return ':s' . $many; #} else { return '=s' . $many; } } sub _shell_arg_usage_string_from_property_meta { my ($self, $property_meta) = @_; my $string = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $string = uc($string); } if ($property_meta->{shell_args_position}) { if ($property_meta->is_optional) { $string = "[$string]"; } } else { $string = "--$string"; if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { $string = "[$string]"; } else { if ($property_meta->is_many) { $string .= "=?[,?]"; } else { $string .= '=?'; } if ($property_meta->is_optional) { $string = "[$string]"; } } } return $string; } sub _shell_arg_getopt_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), #this prevents defaults from being used for is_many properties #($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_arg_getopt_complete_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); my $completions = $property_meta->valid_values; if ($completions) { if (ref($completions) eq 'ARRAY') { $completions = [ @$completions ]; } } else { my $type = $property_meta->data_type; my @complete_as_files = ( 'File','FilePath','Filesystem','FileSystem','FilesystemPath','FileSystemPath', 'Text','String', ); my @complete_as_directories = ( 'Directory','DirectoryPath','Dir','DirPath', ); if (!defined($type)) { $completions = 'files'; } else { for my $pattern (@complete_as_files) { if (!$type || $type eq $pattern) { $completions = 'files'; last; } } for my $pattern (@complete_as_directories) { if ( $type && $type eq $pattern) { $completions = 'directories'; last; } } } } return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), $completions, # ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_args_getopt_specification { my $self = shift; my @getopt; my @params; for my $meta ($self->_shell_args_property_meta) { my ($spec, @params_addition) = $self->_shell_arg_getopt_specification_from_property_meta($meta); push @getopt,$spec; push @params, @params_addition; } @getopt = sort @getopt; return { @params}, @getopt; } sub _shell_args_getopt_complete_specification { my $self = shift; my @getopt; for my $meta ($self->_shell_args_property_meta) { my ($spec, $completions) = $self->_shell_arg_getopt_complete_specification_from_property_meta($meta); push @getopt, $spec, $completions; } return @getopt; } sub _bare_shell_argument_names { my $self = shift; my $meta = $self->__meta__; my @ordered_names = map { $_->property_name } sort { $a->{shell_args_position} <=> $b->{shell_args_position} } grep { $_->{shell_args_position} } $self->_shell_args_property_meta(); return @ordered_names; } # # Logic to turn command-line text into objects for parameter/input values # our %ALTERNATE_FROM_CLASS = (); # This will prevent infinite loops during recursion. our %SEEN_FROM_CLASS = (); our $MESSAGE; sub resolve_param_value_from_cmdline_text { my ($self, $param_info) = @_; my $param_name = $param_info->{name}; my $param_class = $param_info->{class}; my @param_args = @{$param_info->{value}}; my $param_str = join(',', @param_args); if (ref($param_class) eq 'ARRAY') { my @param_class = @$param_class; if (@param_class > 1) { die 'Multiple data types on command arguments are not supported.'; } else { $param_class = $param_class[0]; } } my $param_resolve_message = "Resolving parameter '$param_name' from command argument '$param_str'..."; my $pmeta = $self->__meta__->property($param_name); my $require_user_verify = $pmeta->{'require_user_verify'}; my @results; my $bx = eval { UR::BoolExpr->resolve_for_string($param_class, $param_str) }; my $bx_error = $@; if ($bx) { @results = $param_class->get($bx); if (@results > 1 && !defined($require_user_verify)) { $require_user_verify = 1; } } else { for my $arg (@param_args) { %SEEN_FROM_CLASS = (); # call resolve_param_value_from_text without a via_method to "bootstrap" recursion my @arg_results = $self->resolve_param_value_from_text($arg, $param_class); if (@arg_results != 1 && !defined($require_user_verify)) { $require_user_verify = 1; } push @results, @arg_results; } } if (@results) { # the ALTERNATE_FROM_CLASS stuff leads to non $param_class objects in results @results = List::MoreUtils::uniq(@results); @results = grep { $_->isa($param_class) } @results; $self->status_message($param_resolve_message . " found " . @results); } else { if ($bx_error) { $self->status_message($bx_error); } $self->status_message($param_resolve_message . " none found."); } return unless (@results); my $limit_results_method = "_limit_results_for_$param_name"; if ( $self->can($limit_results_method) ) { @results = $self->$limit_results_method(@results); return unless (@results); } @results = List::MoreUtils::uniq(@results); if ($require_user_verify) { if (!$pmeta->{'is_many'} && @results > 1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "'$param_name' expects only one result."; if ($ENV{UR_NO_REQUIRE_USER_VERIFY}) { die "$MESSAGE\n"; } } @results = $self->_get_user_verification_for_param_value($param_name, @results); } while (!$pmeta->{'is_many'} && @results > 1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "'$param_name' expects only one result, not many!"; @results = $self->_get_user_verification_for_param_value($param_name, @results); } if (wantarray) { return @results; } elsif (not defined wantarray) { return; } elsif (@results > 1) { Carp::confess("Multiple matches found!"); } else { return $results[0]; } } sub resolve_param_value_from_text { my ($self, $param_arg, $param_class, $via_method) = @_; unless ($param_class) { $param_class = $self->class; } $SEEN_FROM_CLASS{$param_class} = 1; my @results; # try getting BoolExpr, otherwise fallback on '_resolve_param_value_from_text_by_name_or_id' parser eval { @results = $self->_resolve_param_value_from_text_by_bool_expr($param_class, $param_arg); }; Carp::croak($@) if ($@ and $@ !~ m/Not a valid BoolExpr/); if (!@results && !$@) { # no result and was valid BoolExpr then we don't want to break it apart because we # could query enormous amounts of info return; } # the first param_arg is all param_args to try BoolExpr so skip if it has commas if (!@results && $param_arg !~ /,/) { my @results_by_string; if ($param_class->can('_resolve_param_value_from_text_by_name_or_id')) { @results_by_string = $param_class->_resolve_param_value_from_text_by_name_or_id($param_arg); } else { @results_by_string = $self->_resolve_param_value_from_text_by_name_or_id($param_class, $param_arg); } push @results, @results_by_string; } # if we still don't have any values then try via alternate class if (!@results && $param_arg !~ /,/) { @results = $self->_resolve_param_value_via_related_class_method($param_class, $param_arg, $via_method); } if ($via_method) { @results = map { $_->$via_method } @results; } if (wantarray) { return @results; } elsif (not defined wantarray) { return; } elsif (@results > 1) { Carp::confess("Multiple matches found!"); } else { return $results[0]; } } sub _resolve_param_value_via_related_class_method { my ($self, $param_class, $param_arg, $via_method) = @_; my @results; my $via_class; if (exists($ALTERNATE_FROM_CLASS{$param_class})) { $via_class = $param_class; } else { for my $class (keys %ALTERNATE_FROM_CLASS) { if ($param_class->isa($class)) { if ($via_class) { $self->error_message("Found additional via_class $class but already found $via_class!"); } $via_class = $class; } } } if ($via_class) { my @from_classes = sort keys %{$ALTERNATE_FROM_CLASS{$via_class}}; while (@from_classes && !@results) { my $from_class = shift @from_classes; my @methods = @{$ALTERNATE_FROM_CLASS{$via_class}{$from_class}}; my $method; if (@methods > 1 && !$via_method && !$ENV{UR_NO_REQUIRE_USER_VERIFY}) { $self->status_message("Trying to find $via_class via $from_class...\n"); my $method_choices; for (my $i = 0; $i < @methods; $i++) { $method_choices .= ($i + 1) . ": " . $methods[$i]; $method_choices .= " [default]" if ($i == 0); $method_choices .= "\n"; } $method_choices .= (scalar(@methods) + 1) . ": none\n"; $method_choices .= "Which method would you like to use?"; my $response = $self->_ask_user_question($method_choices, 0, '\d+', 1, '#'); if ($response =~ /^\d+$/) { $response--; if ($response == @methods) { $method = undef; } elsif ($response >= 0 && $response <= $#methods) { $method = $methods[$response]; } else { $self->error_message("Response was out of bounds, exiting..."); exit; } $ALTERNATE_FROM_CLASS{$via_class}{$from_class} = [$method]; } elsif (!$response) { $self->status_message("Exiting..."); } } else { $method = $methods[0]; } unless($SEEN_FROM_CLASS{$from_class}) { #$self->debug_message("Trying to find $via_class via $from_class->$method..."); @results = eval {$self->resolve_param_value_from_text($param_arg, $from_class, $method)}; } } # END for my $from_class (@from_classes) } # END if ($via_class) return @results; } sub _resolve_param_value_from_text_by_bool_expr { my ($self, $param_class, $arg) = @_; my @results; my $bx = eval { UR::BoolExpr->resolve_for_string($param_class, $arg); }; if ($bx) { @results = $param_class->get($bx); } else { die "Not a valid BoolExpr"; } #$self->debug_message("B: $param_class '$arg' " . scalar(@results)); return @results; } sub _try_get_by_id { my ($self, $param_class, $str) = @_; my $class_meta = $param_class->__meta__; my @id_property_names = $class_meta->id_property_names; if (@id_property_names == 0) { die "Failed to determine ID property names for class ($param_class)."; } elsif (@id_property_names == 1) { my $id_data_type = $class_meta->property_meta_for_name($id_property_names[0])->_data_type_as_class_name || ''; # Validate $str, if possible, to prevent warnings from database if $str does not fit column type. if ($id_data_type->isa('UR::Value::Number')) { # Oracle's Number data type includes floats but we just use integers for numeric IDs return ($str =~ /^[+-]?\d+$/); } } return 1; } sub _resolve_param_value_from_text_by_name_or_id { my ($self, $param_class, $str) = @_; my (@results); if ($self->_try_get_by_id($param_class, $str)) { @results = eval { $param_class->get($str) }; } if (!@results && $param_class->can('name')) { @results = $param_class->get(name => $str); unless (@results) { @results = $param_class->get("name like" => "$str"); } } return @results; } sub _get_user_verification_for_param_value { my ($self, $param_name, @list) = @_; my $n_list = scalar(@list); if ($n_list > 200 && !$ENV{UR_NO_REQUIRE_USER_VERIFY}) { my $response = $self->_ask_user_question("Would you [v]iew all $n_list item(s) for '$param_name', (p)roceed, or e(x)it?", 0, '[v]|p|x', 'v'); if(!$response || $response eq 'x') { $self->status_message("Exiting..."); exit; } return @list if($response eq 'p'); } my @new_list; while (!@new_list) { @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list); } my @ids = map { $_->id } @new_list; $self->status_message("The IDs for your selection are:\n" . join(',', @ids) . "\n\n"); return @new_list; } sub _get_user_verification_for_param_value_drilldown { my ($self, $param_name, @results) = @_; my $n_results = scalar(@results); my $pad = length($n_results); # Allow an environment variable to be set to disable the require_user_verify attribute return @results if ($ENV{UR_NO_REQUIRE_USER_VERIFY}); return if (@results == 0); my @dnames = map {$_->__display_name__} grep { $_->can('__display_name__') } @results; my $max_dname_length = @dnames ? length((sort { length($b) <=> length($a) } @dnames)[0]) : 0; my @statuses = map {$_->status || 'missing_status'} grep { $_->can('status') } @results; my $max_status_length = @statuses ? length((sort { length($b) <=> length($a) } @statuses)[0]) : 0; my @results_with_display_name_and_class = map { [ $_->__display_name__, $_->class, $_ ] } @results; @results = map { $_->[2] } sort { $a->[1] cmp $b->[1] } sort { $a->[0] cmp $b->[0] } @results_with_display_name_and_class; my @classes = List::MoreUtils::uniq(map {$_->class} @results); my $response; my @caller = caller(1); while (!$response) { $self->status_message("\n"); # TODO: Replace this with lister? for (my $i = 1; $i <= $n_results; $i++) { my $param = $results[$i - 1]; my $num = $self->_pad_string($i, $pad); my $msg = "$num:"; $msg .= ' ' . $self->_pad_string($param->__display_name__, $max_dname_length, 'suffix'); my $status = ' '; if ($param->can('status')) { $status = $param->status || 'missing_status'; } $msg .= "\t" . $self->_pad_string($status, $max_status_length, 'suffix'); $msg .= "\t" . $param->class if (@classes > 1); $self->status_message($msg); } if ($MESSAGE) { $MESSAGE = "\n" . '*'x80 . "\n" . $MESSAGE . "\n" . '*'x80 . "\n"; $self->status_message($MESSAGE); $MESSAGE = ''; } my $pretty_values = '(c)ontinue, (h)elp, e(x)it'; my $valid_values = '\*|c|h|x|[-+]?[\d\-\., ]+'; if ($caller[3] =~ /_trim_list_from_response/) { $pretty_values .= ', (b)ack'; $valid_values .= '|b'; } $response = $self->_ask_user_question("Please confirm the above items for '$param_name' or modify your selection.", 0, $valid_values, 'h', $pretty_values.', or specify item numbers to use'); if (lc($response) eq 'h' || !$self->_validate_user_response_for_param_value_verification($response)) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "Help:\n". "* Specify which elements to keep by listing them, e.g. '1,3,12' would keep\n". " items 1, 3, and 12.\n". "* Begin list with a minus to remove elements, e.g. '-1,3,9' would remove\n". " items 1, 3, and 9.\n". "* Ranges can be used, e.g. '-11-17, 5' would remove items 11 through 17 and\n". " remove item 5."; $response = ''; } } if (lc($response) eq 'x') { $self->status_message("Exiting..."); exit; } elsif (lc($response) eq 'b') { return; } elsif (lc($response) eq 'c' | $response eq '*') { return @results; } elsif ($response =~ /^[-+]?[\d\-\., ]+$/) { @results = $self->_trim_list_from_response($response, $param_name, @results); return @results; } else { die $self->error_message("Conditional exception, should not have been reached!"); } } sub terminal_input_filehandle { my $self = shift; my $fh = IO::File->new('/dev/tty', 'r'); unless ($fh) { Carp::carp("Couldn't open /dev/tty for terminal input: $!\n Using STDIN..."); $fh = *STDIN; } return $fh; } sub _ask_user_question { my $self = shift; my $question = shift; my $timeout = shift; my $valid_values = shift || "yes|no"; my $default_value = shift || undef; my $pretty_valid_values = shift || $valid_values; $valid_values = lc($valid_values); my $input; $timeout = 60 unless(defined($timeout)); local $SIG{ALRM} = sub { print STDERR "Exiting, failed to reply to question '$question' within '$timeout' seconds.\n"; exit; }; print STDERR "\n$question\n"; print STDERR "Reply with $pretty_valid_values: "; unless ($self->_can_interact_with_user) { print STDERR "\n"; die $self->error_message("Attempting to ask user question but cannot interact with user!"); } my $terminal = $self->terminal_input_filehandle(); alarm($timeout) if ($timeout); chomp($input = $terminal->getline()); alarm(0) if ($timeout); print STDERR "\n"; if(lc($input) =~ /^$valid_values$/) { return lc($input); } elsif ($default_value) { return $default_value; } else { $self->error_message("'$input' is an invalid answer to question '$question'\n\n"); return; } } sub _validate_user_response_for_param_value_verification { my ($self, $response_text) = @_; $response_text = substr($response_text, 1) if ($response_text =~ /^[+-]/); my @response = split(/[\s\,]/, $response_text); for my $response (@response) { if ($response =~ /^[xbc*]$/) { return 1; } if ($response !~ /^(\d+)([-\.]+(\d+))?$/) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "ERROR: Invalid list provided ($response)"; return 0; } if ($3 && $1 && $3 < $1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "ERROR: Inverted range provided ($1-$3)"; return 0; } } return 1; } sub _trim_list_from_response { my ($self, $response_text, $param_name, @list) = @_; my $method; if ($response_text =~ /^[+-]/) { $method = substr($response_text, 0, 1); $response_text = substr($response_text, 1); } else { $method = '+'; } my @response = split(/[\s\,]/, $response_text); my %indices; @indices{0..$#list} = 0..$#list if ($method eq '-'); for my $response (@response) { $response =~ /^(\d+)([-\.]+(\d+))?$/; my $low = $1; $low--; my $high = $3 || $1; $high--; die if ($high < $low); if ($method eq '+') { @indices{$low..$high} = $low..$high; } else { delete @indices{$low..$high}; } } #$self->debug_message("Indices: " . join(',', sort(keys %indices))); my @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list[sort keys %indices]); unless (@new_list) { @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list); } return @new_list; } sub _pad_string { my ($self, $str, $width, $pos) = @_; $str = '' if ! defined $str; my $padding = $width - length($str); $padding = 0 if ($padding < 0); if ($pos && $pos eq 'suffix') { return $str . ' 'x$padding; } else { return ' 'x$padding . $str; } } sub _can_interact_with_user { my $self = shift; if ( -t STDERR ) { return 1; } else { return 0; } } 1; DynamicSubCommands.pm100664023532023421 1741012544604516 20062 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::DynamicSubCommands; use strict; use warnings; use UR; class Command::DynamicSubCommands { is => 'Command', is_abstract => 1, }; sub _init_subclass { my $subclass = shift; my $meta = $subclass->__meta__; if (grep { $_ eq __PACKAGE__ } $meta->parent_class_names) { my $delegating_class_name = $subclass; eval "sub ${subclass}::_delegating_class_name { '$delegating_class_name' }"; } return 1; } sub __extend_namespace__ { # auto generate sub-classes at the time of first reference my ($self,$ext) = @_; my $meta = $self->SUPER::__extend_namespace__($ext); return $meta if $meta; unless ($self->can('_sub_commands_from')) { die "Class " . $self->class . " does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $self->_sub_commands_from; my $target_class_name = join('::', $ref_class, $ext); my $target_class_meta = UR::Object::Type->get($target_class_name); if ($target_class_meta and $target_class_name->isa($ref_class)) { my $subclass_name = join('::', $self->class, $ext); my $subclass = $self->_build_sub_command($subclass_name, $self->class, $target_class_name); my $meta = $subclass->__meta__; return $meta; } return; } sub _build_all_sub_commands { my ($class) = @_; unless ($class->can('_sub_commands_from')) { die "Class $class does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $class->_sub_commands_from; my $delegating_class_name = $class; my $module = $ref_class; $module =~ s/::/\//g; $module .= '.pm'; my $base_path = $INC{$module}; unless ($base_path) { if (UR::Object::Type->get($ref_class)) { $base_path = $INC{$module}; } unless ($base_path) { die "Failed to find the path for ref class $ref_class!"; } } $base_path =~ s/$module//; my $ref_path = $ref_class; $ref_path =~ s/::/\//g; my $full_ref_path = $base_path . '/' . $ref_path; my @target_paths = glob("$full_ref_path/*.pm"); my @target_class_names; for my $target_path (@target_paths) { my $target = $target_path; $target =~ s#$base_path\/$ref_path/##; $target =~ s/\.pm//; my $target_class_name = $ref_class . '::' . $target; my $target_meta = UR::Object::Type->get($target_class_name); next unless $target_meta; next unless $target_class_name->isa($ref_class); push @target_class_names, $target => $target_class_name; } my %target_classes = @target_class_names; my @subclasses; for my $target (sort keys %target_classes) { my $target_class_name = $target_classes{$target}; my $class_name = $delegating_class_name . '::' . $target; # skip commands which have a module my $module_name = $class_name; $module_name =~ s|::|/|g; $module_name .= '.pm'; if (my @matches = grep { -e $_ . '/' . $module_name } @INC) { my $c = UR::Object::Type->get($class_name); push @subclasses, $class_name; next; } my @new_class_names = $class->_build_sub_command($class_name,$delegating_class_name,$target_class_name); for my $new_class_name (@new_class_names) { eval "sub ${new_class_name}::_target_class_name { '$target_class_name' }"; push @subclasses, $new_class_name; } } return @subclasses; } sub _build_sub_command { my ($self,$class_name,$delegating_class_name,$reference_class_name) = @_; class {$class_name} { is => $delegating_class_name, doc => '', }; return $class_name; } sub sub_command_dirs { my $class = ref($_[0]) || $_[0]; return ( $class eq $class->_delegating_class_name ? 1 : 0 ); } sub sub_command_classes { my $class = shift; unless(exists $class->__meta__->{_sub_commands}) { my @subclasses = $class->_build_all_sub_commands; $class->__meta__->{_sub_commands} = \@subclasses; } return @{ $class->__meta__->{_sub_commands} }; } sub _target_class_name { undef } 1; =pod =head1 NAME Command::DynamicSubCommands - auto-generate sub-commands based on other classes =head1 SYNOPSIS # given that these classes exist: # Acme::Task::Foo # Acme::Task::Bar # in Acme/Worker/Command/DoTask.pm: class Acme::Worker::Command::DoTask { is => 'Command::DynamicSubCommands', has => [ param1 => { is => 'Text' }, param2 => { is => 'Text' }, ] }; sub _sub_commands_from { 'Acme::Task' } sub execute { my $self = shift; print "this command " . ref($self) . " applies to " . $self->_target_class_name; return 1; } # the class above will discover them at compile, # and auto-generate these subclasses of itself: # Acme::Worker::Command::DoTask::Foo # Acme::Worker::Command::DoTask::Bar # in the shell... # # $ acme worker do-task # foo # bar # # $ acme worker do-task foo --param1 aaa --param2 bbb # this command Acme::Worker::Command::DoTask::Foo applies to Acme::Task::Foo # # $ acme worker do-task bar --param1 ccc --param2 ddd # this command Acme::Worker::Command::DoTask::Bar applies to Acme::Task::Bar =head1 DESCRIPTION This module helps you avoid writing boilerplate commands. When a command has a set of sub-commands which are meant to be derived from another group of classes, this module lets you auto-generate those sub-commands at run time. =head1 REQUIRED ABSTRACT METHOD =over 4 =item _sub_commands_from $base_namespace = Acme::Order::Command->_sub_commands_from(); # 'Acme::Task Returns the namespace from which target classes will be discovered, and sub-commands will be generated. =back =head1 PRIVATE API =over 4 =item _target_class_name $c= Acme::Order::Command::Purchasing->_target_class_name; # 'Acme::Task::Foo' The name of some class under the _sub_commands_from() namespace. This value is set during execute, revealing which sub-command the caller is using. =back =head1 OPTIONAL OVERRIDES =over 4 =item _build_sub_commmand This can be overridden to customize the sub-command construction. By default, each target under _sub_commands_from will result in a call to this method. The default implementation is below: my $self = shift; my ($suggested_class_name,$delegator_class_name,$target_class_name) = @_; class {$suggested_class_name} { is => $delegator_class_name, sub_classify_by => 'class', has_constant => [ _target_class_name => { value => $target_class_name }, ] }; return ($suggested_class_name); Note that the class in question may be on the filesystem, and not need to be created. The return list can include more than one class name, or zero class names. =item _build_all_sub_commands This is called once for any class which inherits from Command::DynamicSubCommands. It generates the sub-commands as needed, and returns a list. By default it resolves the target classes, and calls _build_sub_command It can be overridden to customize behavior, or filter results. Be sure to call @cmds = $self->SUPER::_build_all_sub_commands() if you want to get the default commands in addition to overriding. =back The sub-commands need not be 1:1 with the target classes, though this is the default. The sub-commands need not inherit from the Command::DynamicSubCommands base command which generates them, though this is the default. =cut Shell.pm100664023532023421 711712544604516 15374 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::Shell; use strict; use warnings; use Command::V2; class Command::Shell { is => 'Command::V2', is_abstract => 1, subclassify_by => "_shell_command_subclass", has_input => [ delegate_type => { is => 'Text', shell_args_position => 1, doc => 'the class name of the command to be executed' }, argv => { is => 'Text', is_many => 1, is_optional => 1, shell_args_position => 2, doc => 'list of command-line arguments to be translated into parameters' }, ], has_transient => [ delegate => { is => 'Command', doc => 'the command which this adaptor wraps' }, _shell_command_subclass => { calculate_from => ['delegate_type'], calculate => sub { my $delegate_type = shift; my $subclass = $delegate_type . "::Shell"; eval "$subclass->class"; if ($@) { my $new_subclass = UR::Object::Type->define( class_name => $subclass, is => __PACKAGE__ ); die "Failed to fabricate subclass $subclass!" unless $new_subclass; } return $subclass; }, }, ], has_output => [ exit_code => => { is => 'Number', doc => 'the exit code to be returned to the shell', } ], doc => 'an adaptor to create and run commands as specified from a standard command-line shell (bash)' }; sub help_synopsis { return <run("Foo",@ARGV); The run() static method will construct the appropriate Command::Shell object, have it build its delegate, run the delegate's execution method in an in-memory transaction sandbox, and capture an exit code. If the correct environment variables are set, it will respond to a bash tab-completion request, such that the "foo" script can be used as a self-completer. EOS } sub run { my $class = shift; my $delegate_type = shift; my @argv = @_; my $cmd = $class->create(delegate_type => $delegate_type, argv => \@argv); #print STDERR "created $cmd\n"; $cmd->execute; my $exit_code = $cmd->exit_code; $cmd->delete; return $exit_code; } sub execute { my $self = shift; my $delegate_type = $self->delegate_type; eval "use above '$delegate_type'"; if ($@) { my $t = UR::Object::Type->get($delegate_type); unless ($t) { die "Failure to use delegate class $delegate_type!:\n$@"; } } my @argv = $self->argv; my $exit_code = $delegate_type->_cmdline_run(@argv); $self->exit_code($exit_code); return 1; } # TODO: migrate all methods in Command::V2 which live in the Command::Dispatch::Shell module to this package # Methods which address $self to get to shell-specific things still call $self # Methods which address $self to get to the underlying command should instead call $self->delegate 1; SubCommandFactory.pm100664023532023421 725612544604516 17711 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::SubCommandFactory; use strict; use warnings; use UR; class Command::SubCommandFactory { is => 'Command::Tree', is_abstract => 1, doc => 'Base class for commands that delegate to sub-commands that may need to be dynamically created', }; sub _init_subclass { my $subclass = shift; my $meta = $subclass->__meta__; if (grep { $_ eq __PACKAGE__ } $meta->parent_class_names) { my $delegating_class_name = $subclass; eval "sub ${subclass}::_delegating_class_name { '$delegating_class_name' }"; } return 1; } sub _build_sub_command_mapping { my ($class) = @_; unless ($class->can('_sub_commands_from')) { die "Class $class does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $class->_sub_commands_from; my @inheritance; if ($class->can('_sub_commands_inherit_from') and defined $class->_sub_commands_inherit_from) { @inheritance = $class->_sub_commands_inherit_from(); } else { @inheritance = $class; } my $module = $ref_class; $module =~ s/::/\//g; $module .= '.pm'; my $base_path = $INC{$module}; unless ($base_path) { if (UR::Object::Type->get($ref_class)) { $base_path = $INC{$module}; } unless ($base_path) { die "Failed to find the path for ref class $ref_class!"; } } $base_path =~ s/$module//; my $ref_path = $ref_class; $ref_path =~ s/::/\//g; my $full_ref_path = $base_path . '/' . $ref_path; my @target_paths = glob("$full_ref_path/*.pm"); my @target_class_names; for my $target_path (@target_paths) { my $target = $target_path; $target =~ s#$base_path\/$ref_path/##; $target =~ s/\.pm//; my $target_base_class = $class->_target_base_class; my $target_class_name = $target_base_class . '::' . $target; my $target_meta = UR::Object::Type->get($target_class_name); next unless $target_meta; next unless $target_class_name->isa($target_base_class); push @target_class_names, $target => $target_class_name; } my %target_classes = @target_class_names; # Create a mapping of command names to command classes, and either find or # create those command classes my $mapping; for my $target (sort keys %target_classes) { my $target_class_name = $target_classes{$target}; my $command_class_name = $class . '::' . $target; my $command_module_name = $command_class_name; $command_module_name =~ s|::|/|g; $command_module_name .= '.pm'; # If the command class already exists, load it. Otherwise, create one. if (grep { -e $_ . '/' . $command_module_name } @INC) { UR::Object::Type->get($command_class_name); } else { $class->_build_sub_command($command_class_name, @inheritance); } # Created commands need to know where their parameters came from no warnings 'redefine'; eval "sub ${command_class_name}::_target_class_name { '$target_class_name' }"; use warnings; my $command_name = $class->_command_name_for_class_word($target); $mapping->{$command_name} = $command_class_name; } return $mapping; } sub _build_sub_command { my ($self, $class_name, @inheritance) = @_; class {$class_name} { is => \@inheritance, doc => '', }; return $class_name; } sub _target_base_class { return $_[0]->_sub_commands_from; } sub _target_class_name { undef } sub _sub_commands_inherit_from { undef } 1; Test.pm100664023532023421 17212544604516 15216 0ustar00abrummetgsc000000000000UR-0.44/lib/Commanduse strict; use warnings; use UR; use Command; package Command::Test; class Command::Test{ is => 'Command', }; 1; Echo.pm100664023532023421 132012544604516 16110 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Testuse strict; use warnings; use UR; use Command; package Command::Test::Echo; class Command::Test::Echo { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'echo the input back, and die or fail if those words appear in the input', }; sub execute { my $self = shift; print "job " . $self->id . " started at " . $self->__context__->now . "\n"; print STDERR "test error!\n"; for (1..10) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Tree1.pm100664023532023421 27012544604516 16175 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Testuse strict; use warnings; use UR; use Command; package Command::Test::Tree1; class Command::Test::Tree1 { is => 'Command', doc => 'more exciting operations are here' }; 1; Echo1.pm100664023532023421 110412544604516 17151 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Test/Tree1use strict; use warnings; use UR; use Command; package Command::Test::Tree1::Echo1; class Command::Test::Tree1::Echo1 { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'test command 1 to echo output1', }; sub execute { my $self = shift; for (1..6) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Echo2.pm100664023532023421 110312544604516 17151 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Test/Tree1use strict; use warnings; use UR; use Command; package Command::Test::Tree1::Echo2; class Command::Test::Tree1::Echo2 { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'test command 2 to echo output', }; sub execute { my $self = shift; for (1..6) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Tree.pm100664023532023421 4024512544604516 15243 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::Tree; use strict; use warnings; use UR; use File::Basename qw/basename/; our $VERSION = "0.44"; # UR $VERSION; class Command::Tree { is => 'Command::V2', is_abstract => 1, doc => 'base class for commands which delegate to sub-commands', }; sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; if ( $argv[0] and $argv[0] !~ /^\-/ and my $class_for_sub_command = $self->class_for_sub_command($argv[0]) ) { # delegate shift @argv; return $class_for_sub_command->resolve_class_and_params_for_argv(@argv); } elsif ( @argv == 1 and $argv[0] =~ /^(\-)?\-h(elp)?$/ ) { # HELP ME! return ($self, { help => 1 }); } else { # error return ($self,undef); } } sub resolve_option_completion_spec { my $class = shift; my @completion_spec; my @sub = eval { $class->sub_command_names }; if ($@) { $class->warning_message("Couldn't load class $class: $@\nSkipping $class..."); return; } for my $sub (@sub) { my $sub_class = $class->class_for_sub_command($sub); my $sub_tree = $sub_class->resolve_option_completion_spec() if defined($sub_class); # Hack to fix several broken commands, this should be removed once commands are fixed. # If the commands were not broken then $sub_tree will always exist. # Basically if $sub_tree is undef then we need to remove '>' to not break the OPTS_SPEC if ($sub_tree) { push @completion_spec, '>' . $sub => $sub_tree; } else { if (defined $sub_class) { print "WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; } else { print "WARNING: $sub has no sub_class so could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; } push @completion_spec, $sub => undef; } } push @completion_spec, "help!" => undef; return \@completion_spec } sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } return ""; } } sub doc_help { my $self = shift; my $command_name = $self->command_name; my $text; # show the list of sub-commands $text = sprintf( "Sub-commands for %s:\n%s", Term::ANSIColor::colored($command_name, 'bold'), $self->help_sub_commands, ); return $text; } sub doc_manual { my $self = shift; my $pod = $self->_doc_name_version; my $manual = $self->_doc_manual_body; my $help = $self->help_detail; if ($manual or $help) { $pod .= "=head1 DESCRIPTION:\n\n"; my $txt = $manual || $help; if ($txt =~ /^\=/) { # pure POD $pod .= $manual; } else { $txt =~ s/\n/\n\n/g; $pod .= $txt; #$pod .= join('', map { " $_\n" } split ("\n",$txt)) . "\n"; } } my $sub_commands = $self->help_sub_commands(brief => 1); $pod .= "=head1 SUB-COMMANDS\n\n" . $sub_commands . "\n\n"; $pod .= $self->_doc_footer(); $pod .= "\n\n=cut\n\n"; return "\n$pod"; } sub sorted_sub_command_classes { no warnings; my @c = map { [ $_->sub_command_sort_position, $_ ] } shift->sub_command_classes; return map { $_->[1] } sort { ($a->[0] <=> $b->[0]) || ($a->[0] cmp $b->[0]) } @c; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub _categorize_sub_commands { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @order; for my $sub_command_class (@sub_command_classes) { next if $sub_command_class->_is_hidden_in_docs(); my $category = $sub_command_class->sub_command_category || ''; unless (exists $categories{$category}) { if ($category) { push(@order, $category) } else { unshift(@order, ''); } $categories{$category} = []; } push(@{$categories{$category}}, $sub_command_class); } return (\@order, \%categories); } sub help_sub_commands { my ($self, %params) = @_; my ($order, $categories) = $self->_categorize_sub_commands(@_); my $command_name_method = 'command_name_brief'; no warnings; local $Text::Wrap::columns = 60; my @full_data; for my $category (@$order) { my $sub_commands_within_this_category = $categories->{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, ($_->isa('Command::Tree') ? '...' : ''), #$_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= $colors[$c] ? Term::ANSIColor::colored($row->[$c], $colors[$c]) : $row->[$c]; $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } return $text; } sub doc_sub_commands { my $self = shift; my ($order, $categories) = $self->_categorize_sub_commands(@_); my $text = ""; my $indent_lvl = 4; for my $category (@$order) { my $category_name = ($category ? uc $category : "GENERAL"); $text .= "=head2 $category_name\n\n"; for my $cmd (@{$categories->{$category}}) { $text .= "=over $indent_lvl\n\n"; my $name = $cmd->command_name_brief; my $link = $cmd->command_name; $link =~ s/ /-/g; my $description = $cmd->help_brief; $text .= "=item B>\n\n=over 2\n\n=item $description\n\n=back\n\n"; $text .= "=back\n\nE<10>\n\n"; } } return $text; } # # The following methods build allow a command to determine its # sub-commands, if there are any. # # This is for cases in which the Foo::Bar command delegates to # Foo::Bar::Baz, Foo::Bar::Buz or Foo::Bar::Doh, depending on its paramters. sub sub_command_dirs { my $class = shift; my $subdir = ref($class) || $class; $subdir =~ s|::|\/|g; my @dirs = grep { -d $_ } map { $_ . '/' . $subdir } @INC; return @dirs; } sub sub_command_classes { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return values %$mapping; } # For compatability with Command::V1-based callers sub is_sub_command_delegator { return scalar(shift->sub_command_classes); } sub command_tree_source_classes { # override in subclass if you want different sources my $class = shift; return $class; } sub _build_sub_command_mapping { my $class = shift; $class = ref($class) || $class; my @source_classes = $class->command_tree_source_classes; my $mapping; do { no strict 'refs'; $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; if (ref($mapping) eq 'HASH') { return $mapping; } }; for my $source_class (@source_classes) { # check if this class is valid eval{ $source_class->class; }; if ( $@ ) { warn $@; } # for My::Foo::Command::* commands and sub-trees my $subdir = $source_class; $subdir =~ s|::|\/|g; # for My::Foo::*::Command sub-trees my $source_class_above = $source_class; $source_class_above =~ s/::Command//; my $subdir2 = $source_class_above; $subdir2 =~ s|::|/|g; # check everywhere for my $lib (@INC) { my $subdir_full_path = $lib . '/' . $subdir; # find My::Foo::Command::* if (-d $subdir_full_path) { my @files = glob($subdir_full_path . '/*'); for my $file (@files) { my $basename = basename($file); $basename =~ s/.pm$// or next; my $sub_command_class_name = $source_class . '::' . $basename; my $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); unless ($sub_command_class_meta) { local $SIG{__DIE__}; local $SIG{__WARN__}; # until _use_safe is refactored to be permissive, use directly... print ">> $sub_command_class_name\n"; eval "use $sub_command_class_name"; } $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); next unless $sub_command_class_name->isa("Command"); next if $sub_command_class_meta->is_abstract; next if $sub_command_class_name eq $class; my $name = $source_class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } # find My::Foo::*::Command $subdir_full_path = $lib . '/' . $subdir2; my $pattern = $subdir_full_path . '/*/Command.pm'; my @paths = glob($pattern); for my $file (@paths) { next unless defined $file; next unless length $file; next unless -f $file; my $last_word = File::Basename::basename($file); $last_word =~ s/.pm$// or next; my $dir = File::Basename::dirname($file); my $second_to_last_word = File::Basename::basename($dir); my $sub_command_class_name = $source_class_above . '::' . $second_to_last_word . '::' . $last_word; next unless $sub_command_class_name->isa('Command'); next if $sub_command_class_name->__meta__->is_abstract; next if $sub_command_class_name eq $class; my $basename = $second_to_last_word; $basename =~ s/.pm$//; my $name = $source_class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } } return $mapping; } sub sub_command_names { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return keys %$mapping; } sub _try_command_class_named { my $self = shift; my $sub_class = join('::', @_); my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; # If it starts with a "-", then it's a command-line option # First attempt is to convert $sub_command into a camel-case module name # and just try loading it my $name_for_sub_command = join("", map { ucfirst($_) } split(/-/, $sub_command)); my @class_name_parts = (split(/::/,$class), $name_for_sub_command); my $sub_command_class = $self->_try_command_class_named(@class_name_parts); return $sub_command_class if $sub_command_class; # Remove "Command" if it's embedded in the middle and try inserting it in other places, starting at the end @class_name_parts = ( ( map { $_ eq 'Command' ? () : $_ } @class_name_parts) , 'Command'); for(my $i = $#class_name_parts; $i > 0; $i--) { $sub_command_class = $self->_try_command_class_named(@class_name_parts); return $sub_command_class if $sub_command_class; $class_name_parts[$i] = $class_name_parts[$i-1]; $class_name_parts[$i-1] = 'Command'; } # Didn't find it yet. Try exhaustively loading all the command modules under $class my $mapping = $class->_build_sub_command_mapping; if (my $sub_command_class = $mapping->{$sub_command}) { return $sub_command_class; } else { return; } } my $depth = 0; sub __extend_namespace__ { my ($self,$ext) = @_; my $meta = $self->SUPER::__extend_namespace__($ext); return $meta if $meta; $depth++; if ($depth>1) { $depth--; return; } my $class = Command::Tree::class_for_sub_command((ref $self || $self), $self->_command_name_for_class_word($ext)); return $class->__meta__ if $class; return; } 1; __END__ =pod =head1 NAME Command::Tree -base class for commands which delegate to a list of sub-commands =head1 DESCRIPTION # in Foo.pm class Foo { is => 'Command::Tree' }; # in Foo/Cmd1.pm class Foo::Cmd1 { is => 'Command' }; # in Foo/Cmd2.pm class Foo::Cmd2 { is => 'Command' }; # in the shell $ foo cmd1 cmd2 $ foo cmd1 $ foo cmd2 =cut V1.pm100664023532023421 13374712544604516 14664 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::V1; use strict; use warnings; use UR; use Data::Dumper; use File::Basename; use Getopt::Long; use Term::ANSIColor qw(); require Text::Wrap; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['Command', 'Command::Common'], is_abstract => 1, attributes_have => [ is_input => { is => 'Boolean', is_optional => 1 }, is_output => { is => 'Boolean', is_optional => 1 }, is_param => { is => 'Boolean', is_optional => 1 }, shell_args_position => { is => 'Integer', is_optional => 1, doc => 'when set, this property is a positional argument when run from a shell' }, ], has_optional => [ debug => { is => 'Boolean', doc => 'enable debug messages' }, is_executed => { is => 'Boolean' }, result => { is => 'Scalar', is_output => 1 }, original_command_line => { is => 'String', doc => 'null-byte separated list of command and arguments when run via execute_with_shell_params_and_exit'}, ], ); # This is changed with "local" where used in some places $Text::Wrap::columns = 100; # Required for color output eval { binmode STDOUT, ":utf8"; binmode STDERR, ":utf8"; }; sub _init_subclass { # Each Command subclass has an automatic wrapper around execute(). # This ensures it can be called as a class or instance method, # and that proper handling occurs around it. my $subclass_name = $_[0]; no strict; no warnings; if ($subclass_name->can('execute')) { # NOTE: manipulating %{ $subclass_name . '::' } directly causes ptkdb to segfault perl my $new_symbol = "${subclass_name}::_execute_body"; my $old_symbol = "${subclass_name}::execute"; *$new_symbol = *$old_symbol; undef *$old_symbol; } else { #print "no execute in $subclass_name\n"; } if($subclass_name->can('shortcut')) { my $new_symbol = "${subclass_name}::_shortcut_body"; my $old_symbol = "${subclass_name}::shortcut"; *$new_symbol = *$old_symbol; undef *$old_symbol; } return 1; } # # Standard external interface for shell dispatchers # # TODO: abstract out all dispatchers for commands into a given API sub execute_with_shell_params_and_exit { # This automatically parses command-line options and "does the right thing": my $class = shift; if (@_) { die qq| No params expected for execute_with_shell_params_and_exit(). Usage: #!/usr/bin/env perl use My::Command; My::Command->execute_with_shell_params_and_exit; |; } $Command::entry_point_class ||= $class; $Command::entry_point_bin ||= File::Basename::basename($0); if ($ENV{COMP_CWORD}) { require Getopt::Complete; my @spec = $class->resolve_option_completion_spec(); my $options = Getopt::Complete::Options->new(@spec); $options->handle_shell_completion; die "error: failed to exit after handling shell completion!"; } my @argv = @ARGV; @ARGV = (); my $exit_code; eval { $exit_code = $class->_execute_with_shell_params_and_return_exit_code(@argv); UR::Context->commit or die "Failed to commit!: " . UR::Context->error_message(); }; if ($@) { $class->error_message($@); UR::Context->rollback or die "Failed to rollback changes after failed commit!!!\n"; $exit_code = 255 unless ($exit_code); } exit $exit_code; } sub _execute_with_shell_params_and_return_exit_code { my $class = shift; my @argv = @_; my $original_cmdline = join("\0",$0,@argv); # make --foo=bar equivalent to --foo bar @argv = map { ($_ =~ /^(--\w+?)\=(.*)/) ? ($1,$2) : ($_) } @argv; my ($delegate_class, $params,$error_tag_list) = $class->resolve_class_and_params_for_argv(@argv); my $rv; if ($error_tag_list and @$error_tag_list) { $class->error_message("There were problems resolving some command-line parameters:\n\t" . join("\n\t", map { my($props,$type,$desc) = @$_{'properties','type','desc'}; "Property '" . join("','",@$props) . "' ($type): $desc" } @$error_tag_list)); } else { $rv = $class->_execute_delegate_class_with_params($delegate_class,$params,$original_cmdline); } my $exit_code = $delegate_class->exit_code_for_return_value($rv); return $exit_code; } # this is called by both the shell dispatcher and http dispatcher for now sub _execute_delegate_class_with_params { my ($class, $delegate_class, $params, $original_cmdline) = @_; unless ($delegate_class) { $class->usage_message($class->help_usage_complete_text); return; } $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); $delegate_class->dump_usage_messages(1); $delegate_class->dump_debug_messages(0); if ( $delegate_class->is_sub_command_delegator && !defined($params) ) { my $command_name = $delegate_class->command_name; $delegate_class->status_message($delegate_class->help_usage_complete_text); $delegate_class->error_message("Please specify a valid sub-command for '$command_name'."); return; } if ( $params->{help} ) { $delegate_class->usage_message($delegate_class->help_usage_complete_text); return 1; } $params->{'original_command_line'} = $original_cmdline if (defined $original_cmdline); my $command_object = $delegate_class->create(%$params); unless ($command_object) { # The delegate class should have emitted an error message. # This is just in case the developer is sloppy, and the user will think the task did not fail. print STDERR "Exiting.\n"; return; } $command_object->dump_status_messages(1); $command_object->dump_warning_messages(1); $command_object->dump_error_messages(1); $command_object->dump_debug_messages($command_object->debug); if ($command_object->debug) { UR::ModuleBase->dump_debug_messages($command_object->debug); } my $rv = $command_object->execute($params); if ($command_object->__errors__) { $command_object->delete; } return $rv; } # # Methods to override in concrete subclasses. # # Override "execute" or "_execute_body" to implement the body of the command. # See above for details of internal implementation. # By default, there are no bare arguments. sub _bare_shell_argument_names { my $self = shift; my $meta = $self->__meta__; my @ordered_names = map { $_->property_name } sort { $a->{shell_args_position} <=> $b->{shell_args_position} } grep { $_->{shell_args_position} } $self->_shell_args_property_meta(); return @ordered_names; } sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } if ($self->is_sub_command_delegator) { return ""; } else { return "no description!!!: define 'doc' in $self"; } } } sub help_synopsis { my $self = shift; return ''; } sub help_detail { my $self = shift; return "!!! define help_detail() in module " . ref($self) || $self . "!"; } sub sub_command_category { return; } sub sub_command_sort_position { # override to do something besides alpha sorting by name return '9999999999 ' . $_[0]->command_name_brief; } # # Self reflection # sub is_abstract { # Override when writing an subclass which is also abstract. my $self = shift; my $class_meta = $self->__meta__; return $class_meta->is_abstract; } sub is_executable { my $self = shift; if ($self->can("_execute_body") eq __PACKAGE__->can("_execute_body")) { return; } elsif ($self->is_abstract) { return; } else { return 1; } } sub is_sub_command_delegator { my $self = shift; if (scalar($self->sub_command_dirs)) { return 1; } else { return; } } sub _time_now { # return the current time in context # this may not be the real time in selected cases shift->__context__->now; } sub color_command_name { my $text = shift; my $colored_text = []; my @COLOR_TEMPLATES = ('red', 'bold red', 'magenta', 'bold magenta'); my @parts = split(/\s+/, $text); for(my $i = 0 ; $i < @parts ; $i++ ){ push @$colored_text, ($i < @COLOR_TEMPLATES) ? Term::ANSIColor::colored($parts[$i], $COLOR_TEMPLATES[$i]) : $parts[$i]; } return join(' ', @$colored_text); } sub _base_command_class_and_extension { my $self = shift; my $class = ref($self) || $self; return ($class =~ /^(.*)::([^\:]+)$/); } sub _command_name_for_class_word { my $self = shift; my $s = shift; $s =~ s/_/-/g; $s =~ s/^([A-Z])/\L$1/; # ignore first capital because that is assumed $s =~ s/([A-Z])/-$1/g; # all other capitals prepend a dash $s =~ s/([a-zA-Z])([0-9])/$1$2/g; # treat number as begining word $s = lc($s); return $s; } sub command_name { my $self = shift; my $class = ref($self) || $self; my $prepend = ''; if (defined($Command::entry_point_class) and $class =~ /^($Command::entry_point_class)(::.+|)$/) { $prepend = $Command::entry_point_bin; $class = $2; if ($class =~ s/^:://) { $prepend .= ' '; } } my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } @words); return $prepend . $n; } sub command_name_brief { my $self = shift; my $class = ref($self) || $self; my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } $words[-1]); return $n; } # # Methods to transform shell args into command properties # my $_resolved_params_from_get_options = {}; sub _resolved_params_from_get_options { return $_resolved_params_from_get_options; } sub resolve_option_completion_spec { my $class = shift; my @completion_spec; if ($class->is_sub_command_delegator) { my @sub = eval { $class->sub_command_names}; if ($@) { $class->warning_message("Couldn't load class $class: $@\nSkipping $class..."); return; } for my $sub (@sub) { my $sub_class = $class->class_for_sub_command($sub); my $sub_tree = $sub_class->resolve_option_completion_spec() if defined($sub_class); # Hack to fix several broken commands, this should be removed once commands are fixed. # If the commands were not broken then $sub_tree will always exist. # Basically if $sub_tree is undef then we need to remove '>' to not break the OPTS_SPEC if ($sub_tree) { push @completion_spec, '>' . $sub => $sub_tree; } else { print "WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; push @completion_spec, $sub => undef; } } push @completion_spec, "help!" => undef; } else { my $params_hash; @completion_spec = $class->_shell_args_getopt_complete_specification; no warnings; unless (grep { /^help\W/ } @completion_spec) { push @completion_spec, "help!" => undef; } } return \@completion_spec } sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; if ($self->is_sub_command_delegator) { if ( $argv[0] and $argv[0] !~ /^\-/ and my $class_for_sub_command = $self->class_for_sub_command($argv[0]) ) { # delegate shift @argv; return $class_for_sub_command->resolve_class_and_params_for_argv(@argv); } if (@argv) { # this has sub-commands, and is also executable # fall through to the execution_logic... } else { #$self->error_message( # 'Bad command "' . $sub_command . '"' # , "\ncommands:" # , $self->help_sub_commands #); return ($self,undef); } } my ($params_hash,@spec) = $self->_shell_args_getopt_specification; unless (grep { /^help\W/ } @spec) { push @spec, "help!"; } # Thes nasty GetOptions modules insist on working on # the real @ARGV, while we like a little more flexibility. # Not a problem in Perl. :) (which is probably why it was never fixed) local @ARGV; @ARGV = @argv; do { # GetOptions also likes to emit warnings instead of return a list of errors :( my @errors; local $SIG{__WARN__} = sub { push @errors, @_ }; ## Change the pattern to be '--', '-' followed by a non-digit, or '+'. ## This s the effect of treating a negative number as a value of an option. ## This means that we won't be allowed to have an option named, say, -1. ## But since command modules' properties have to be allowable function names, ## and "1" is not a valid function name, it's not really a problem #Getopt::Long::Configure('prefix_pattern=--|-(?!\D)|\+'); unless (GetOptions($params_hash,@spec)) { Carp::croak( join("\n", @errors) ); } }; # Q: Is there a standard getopt spec for capturing non-option paramters? # Perhaps that's not getting "options" :) # A: Yes. Use '<>'. But we need to process this anyway, so it won't help us. if (my @names = $self->_bare_shell_argument_names) { for (my $n=0; $n < @ARGV; $n++) { my $name = $names[$n]; unless ($name) { $self->error_message("Unexpected bare arguments: @ARGV[$n..$#ARGV]!"); return($self, undef); } my $value = $ARGV[$n]; my $meta = $self->__meta__->property_meta_for_name($name); if ($meta->is_many) { if ($n == $#names) { # slurp the rest $params_hash->{$name} = [@ARGV[$n..$#ARGV]]; last; } else { die "has-many property $name is not last in bare_shell_argument_names for $self?!"; } } else { $params_hash->{$name} = $value; } } } elsif (@ARGV) { ## argv but no names $self->error_message("Unexpected bare arguments: @ARGV!"); return($self, undef); } for my $key (keys %$params_hash) { # handle any has-many comma-sep values my $value = $params_hash->{$key}; if (ref($value)) { my @new_value; for my $v (@$value) { my @parts = split(/,\s*/,$v); push @new_value, @parts; } @$value = @new_value; } elsif ($value eq q('') or $value eq q("")) { # Handle the special values '' and "" to mean undef/NULL $params_hash->{$key} = ''; } # turn dashes into underscores my $new_key = $key; next unless ($new_key =~ tr/-/_/); if (exists $params_hash->{$new_key} && exists $params_hash->{$key}) { # this corrects a problem where is_many properties badly interact # with bare args leaving two entries in the hash like: # a-bare-opt => [], a_bare_opt => ['with','vals'] delete $params_hash->{$key}; next; } $params_hash->{$new_key} = delete $params_hash->{$key}; } $_resolved_params_from_get_options = $params_hash; return $self, $params_hash; } # # Methods which let the command auto-document itself. # sub help_usage_complete_text { my $self = shift; my $command_name = $self->command_name; my $text; if (not $self->is_executable) { # no execute implemented if ($self->is_sub_command_delegator) { # show the list of sub-commands $text = sprintf( "Sub-commands for %s:\n%s", Term::ANSIColor::colored($command_name, 'bold'), $self->help_sub_commands, ); } else { # developer error my (@sub_command_dirs) = $self->sub_command_dirs; if (grep { -d $_ } @sub_command_dirs) { $text .= "No execute() implemented in $self, and no sub-commands found!" } else { $text .= "No execute() implemented in $self, and no directory of sub-commands found!" } } } else { # standard: update this to do the old --help format my $synopsis = $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0); my $optional_args = $self->help_options(is_optional => 1); my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; $text = sprintf( "\n%s\n%s\n\n%s%s%s%s%s\n", Term::ANSIColor::colored('USAGE', 'underline'), Text::Wrap::wrap( ' ', ' ', Term::ANSIColor::colored($self->command_name, 'bold'), $self->_shell_args_usage_string || '', ), ( $synopsis ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SYNOPSIS", 'underline'), $synopsis) : '' ), ( $required_args ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED ARGUMENTS", 'underline'), $required_args) : '' ), ( $optional_args ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL ARGUMENTS", 'underline'), $optional_args) : '' ), sprintf( "%s\n%s\n", Term::ANSIColor::colored("DESCRIPTION", 'underline'), Text::Wrap::wrap(' ', ' ', $self->help_detail || '') ), ( $sub_commands ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SUB-COMMANDS", 'underline'), $sub_commands) : '' ), ); } return $text; } sub doc_sections { my $self = shift; my @sections; my $command_name = $self->command_name; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $help_brief = $self->help_brief; my $datetime = $self->__context__->now; my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; my ($date,$time) = split(' ',$datetime); push(@sections, UR::Doc::Section->create( title => "NAME", content => "$command_name" . ($help_brief ? " - $help_brief" : ""), format => "pod", )); push(@sections, UR::Doc::Section->create( title => "VERSION", content => "This document " # separated to trick the version updater . "describes $command_name " . ($version ? "version $version " : "") . "($date at $time)", format => "pod", )); if ($sub_commands) { push(@sections, UR::Doc::Section->create( title => "SUB-COMMANDS", content => $sub_commands, format => 'pod', )); } else { my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; if ($synopsis) { push(@sections, UR::Doc::Section->create( title => "SYNOPSIS", content => $synopsis, format => 'pod' )); } my $required_args = $self->help_options(is_optional => 0, format => "pod"); if ($required_args) { push(@sections, UR::Doc::Section->create( title => "REQUIRED ARGUMENTS", content => "=over\n\n$required_args\n\n=back\n\n", format => 'pod' )); } my $optional_args = $self->help_options(is_optional => 1, format => "pod"); if ($optional_args) { push(@sections, UR::Doc::Section->create( title => "OPTIONAL ARGUMENTS", content => "=over\n\n$optional_args\n\n=back\n\n", format => 'pod' )); } push(@sections, UR::Doc::Section->create( title => "DESCRIPTION", content => join('', map { " $_\n" } split ("\n",$self->help_detail)), format => 'pod', )); } return @sections; } sub help_usage_command_pod { my $self = shift; my $command_name = $self->command_name; my $pod; if (0) { # (not $self->is_executable) # no execute implemented if ($self->is_sub_command_delegator) { # show the list of sub-commands $pod = "Commands:\n" . $self->help_sub_commands; } else { # developer error my (@sub_command_dirs) = $self->sub_command_dirs; if (grep { -d $_ } @sub_command_dirs) { $pod .= "No execute() implemented in $self, and no sub-commands found!" } else { $pod .= "No execute() implemented in $self, and no directory of sub-commands found!" } } } else { # standard: update this to do the old --help format my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0, format => "pod"); my $optional_args = $self->help_options(is_optional => 1, format => "pod"); my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; my $help_brief = $self->help_brief; my $version = do { no strict; ${ $self->class . '::VERSION' } }; $pod = "\n=pod" . "\n\n=head1 NAME" . "\n\n" . $self->command_name . ($help_brief ? " - " . $self->help_brief : '') . "\n\n"; if ($version) { $pod .= "\n\n=head1 VERSION" . "\n\n" . "This document " # separated to trick the version updater . "describes " . $self->command_name . " version " . $version . '.' . "\n\n"; } if ($sub_commands) { $pod .= ( $sub_commands ? "=head1 SUB-COMMANDS\n\n" . $sub_commands . "\n\n" : '' ) } else { $pod .= ( $synopsis ? "=head1 SYNOPSIS\n\n" . $synopsis . "\n\n" : '' ) . ( $required_args ? "=head1 REQUIRED ARGUMENTS\n\n=over\n\n" . $required_args . "\n\n=back\n\n" : '' ) . ( $optional_args ? "=head1 OPTIONAL ARGUMENTS\n\n=over\n\n" . $optional_args . "\n\n=back\n\n" : '' ) . "=head1 DESCRIPTION:\n\n" . join('', map { " $_\n" } split ("\n",$self->help_detail)) . "\n"; } $pod .= "\n\n=cut\n\n"; } return "\n$pod"; } sub help_header { my $class = shift; return sprintf("%s - %-80s\n", $class->command_name ,$class->help_brief ) } sub help_options { my $self = shift; my %params = @_; my $format = delete $params{format}; my @property_meta = $self->_shell_args_property_meta(%params); my @data; my $max_name_length = 0; for my $property_meta (@property_meta) { my $param_name = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $param_name = uc($param_name); } #$param_name = "--$param_name"; my $doc = $property_meta->doc; my $valid_values = $property_meta->valid_values; my $example_values = $property_meta->example_values; unless ($doc) { # Maybe a parent class has documentation for this property eval { foreach my $ancestor_class_meta ( $property_meta->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property_meta->property_name); if ($ancestor_property_meta and $doc = $ancestor_property_meta->doc) { last; } } }; } if (!$doc) { if (!$valid_values) { $doc = "(undocumented)"; } else { $doc = ''; } } if ($valid_values) { $doc .= "\nvalid values:\n"; for my $v (@$valid_values) { $doc .= " " . $v . "\n"; $max_name_length = length($v)+2 if $max_name_length < length($v)+2; } chomp $doc; } if ($example_values && @$example_values) { $doc .= "\nexample" . (@$example_values > 1 and 's') . ":\n"; $doc .= join(', ', map { ref($_) ? Data::Dumper->new([$_])->Terse(1)->Dump() : $_ } @$example_values ); chomp($doc); } $max_name_length = length($param_name) if $max_name_length < length($param_name); my $param_type = $property_meta->data_type || ''; if (defined($param_type) and $param_type !~ m/::/) { $param_type = ucfirst(lc($param_type)); } my $default_value; if (defined($default_value = $property_meta->default_value) || defined(my $calculated_default = $property_meta->calculated_default) ) { unless (defined $default_value) { $default_value = $calculated_default->() } if ($param_type eq 'Boolean') { $default_value = $default_value ? "'true'" : "'false' (--no$param_name)"; } elsif ($property_meta->is_many && ref($default_value) eq 'ARRAY') { if (@$default_value) { $default_value = "('" . join("','",@$default_value) . "')"; } else { $default_value = "()"; } } else { $default_value = "'$default_value'"; } $default_value = "\nDefault value $default_value if not specified"; } push @data, [$param_name, $param_type, $doc, $default_value]; if ($param_type eq 'Boolean') { push @data, ['no'.$param_name, $param_type, "Make $param_name 'false'" ]; } } my $text = ''; for my $row (@data) { if (defined($format) and $format eq 'pod') { $text .= "\n=item " . $row->[0] . ($row->[1]? ' I<' . $row->[1] . '>' : '') . "\n\n" . $row->[2] . "\n". ($row->[3]? $row->[3] . "\n" : ''); } elsif (defined($format) and $format eq 'html') { $text .= "\n\t
" . $row->[0] . ($row->[1]? ' ' . $row->[1] . '' : '') . "
" . $row->[2] . ($row->[3]? "
" . $row->[3] : '') . "
\n"; } else { $text .= sprintf( " %s\n%s\n", Term::ANSIColor::colored($row->[0], 'bold') . " " . $row->[1], Text::Wrap::wrap( " ", # 1st line indent, " ", # all other lines indent, $row->[2], $row->[3] || '', ), ); } } return $text; } sub sorted_sub_command_classes { no warnings; my @c = shift->sub_command_classes; my @commands_with_position = map { [ $_->sub_command_sort_position, $_ ] } @c; my @sorted = sort { $a->[0] <=> $b->[0] || $a->[0] cmp $b->[0] } @commands_with_position; return map { $_->[1] } @sorted; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub help_sub_commands { my $class = shift; my %params = @_; my $command_name_method = 'command_name_brief'; #my $command_name_method = ($params{brief} ? 'command_name_brief' : 'command_name'); my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @categories; for my $sub_command_class (@sub_command_classes) { my $category = $sub_command_class->sub_command_category; $category = '' if not defined $category; next if $sub_command_class->_is_hidden_in_docs(); my $sub_commands_within_category = $categories{$category}; unless ($sub_commands_within_category) { if (defined $category and length $category) { push @categories, $category; } else { unshift @categories,''; } $sub_commands_within_category = $categories{$category} = []; } push @$sub_commands_within_category,$sub_command_class; } no warnings; local $Text::Wrap::columns = 60; my $full_text = ''; my @full_data; for my $category (@categories) { my $sub_commands_within_this_category = $categories{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, $_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= Term::ANSIColor::colored($row->[$c], $colors[$c]), $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } return $text; } sub _is_hidden_in_docs { return; } # # Methods which transform command properties into shell args (getopt) # sub _shell_args_property_meta { my $self = shift; my $class_meta = $self->__meta__; # Find which property metas match the rules. We have to do it this way # because just calling 'get_all_property_metas()' will product multiple matches # if a property is overridden in a child class my $rule = UR::Object::Property->define_boolexpr(@_); my %seen; my (@positional,@required,@optional); foreach my $property_meta ( $class_meta->get_all_property_metas() ) { my $property_name = $property_meta->property_name; next if $seen{$property_name}++; next unless $rule->evaluate($property_meta); next if $property_name eq 'id'; next if $property_name eq 'result'; next if $property_name eq 'is_executed'; next if $property_name eq 'original_command_line'; next if $property_name =~ /^_/; next if defined($property_meta->data_type) and $property_meta->data_type =~ /::/; next if not $property_meta->is_mutable; next if $property_meta->is_delegated; next if $property_meta->is_calculated; # next if $property_meta->{is_output}; # TODO: This was breaking the G::M::T::Annotate::TranscriptVariants annotator. This should probably still be here but temporarily roll back next if $property_meta->is_transient; next if $property_meta->is_constant; if ($property_meta->{shell_args_position}) { push @positional, $property_meta; } elsif ($property_meta->is_optional) { push @optional, $property_meta; } else { push @required, $property_meta; } } my @result; @required = map { [ $_->property_name, $_ ] } @required; @optional = map { [ $_->property_name, $_ ] } @optional; @positional = map { [ $_->{shell_args_position}, $_ ] } @positional; @result = ( (sort { $a->[0] cmp $b->[0] } @required), (sort { $a->[0] cmp $b->[0] } @optional), (sort { $a->[0] <=> $b->[0] } @positional), ); return map { $_->[1] } @result; } sub _shell_arg_name_from_property_meta { my ($self, $property_meta,$singularize) = @_; my $property_name = ($singularize ? $property_meta->singular_name : $property_meta->property_name); my $param_name = $property_name; $param_name =~ s/_/-/g; return $param_name; } sub _shell_arg_getopt_qualifier_from_property_meta { my ($self, $property_meta) = @_; my $many = ($property_meta->is_many ? '@' : ''); if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { return '!' . $many; } #elsif ($property_meta->is_optional) { # return ':s' . $many; #} else { return '=s' . $many; } } sub _shell_arg_usage_string_from_property_meta { my ($self, $property_meta) = @_; my $string = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $string = uc($string); } if ($property_meta->{shell_args_position}) { if ($property_meta->is_optional) { $string = "[$string]"; } } else { $string = "--$string"; if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { $string = "[$string]"; } else { if ($property_meta->is_many) { $string .= "=?[,?]"; } else { $string .= '=?'; } if ($property_meta->is_optional) { $string = "[$string]"; } } } return $string; } sub _shell_arg_getopt_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_arg_getopt_complete_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); my $completions = $property_meta->valid_values; if ($completions) { if (ref($completions) eq 'ARRAY') { $completions = [ @$completions ]; } } else { my $type = $property_meta->data_type; my @complete_as_files = ( 'File','FilePath','Filesystem','FileSystem','FilesystemPath','FileSystemPath', 'Text','String', ); my @complete_as_directories = ( 'Directory','DirectoryPath','Dir','DirPath', ); if (!defined($type)) { $completions = 'files'; } else { for my $pattern (@complete_as_files) { if (!$type || $type eq $pattern) { $completions = 'files'; last; } } for my $pattern (@complete_as_directories) { if ( $type && $type eq $pattern) { $completions = 'directories'; last; } } } } return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), $completions, # ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_args_getopt_specification { my $self = shift; my @getopt; my @params; for my $meta ($self->_shell_args_property_meta) { my ($spec, @params_addition) = $self->_shell_arg_getopt_specification_from_property_meta($meta); push @getopt,$spec; push @params, @params_addition; } @getopt = sort @getopt; return { @params}, @getopt; } sub _shell_args_getopt_complete_specification { my $self = shift; my @getopt; for my $meta ($self->_shell_args_property_meta) { my ($spec, $completions) = $self->_shell_arg_getopt_complete_specification_from_property_meta($meta); push @getopt, $spec, $completions; } return @getopt; } sub _shell_args_usage_string { my $self = shift; if ($self->is_executable) { return join( " ", map { $self->_shell_arg_usage_string_from_property_meta($_) } $self->_shell_args_property_meta() ); } elsif ($self->is_sub_command_delegator) { my @names = $self->sub_command_names; return "[" . join("|",@names) . "] ..." } else { return "(no execute or sub commands implemented)" } return ""; } sub _shell_args_usage_string_abbreviated { my $self = shift; if ($self->is_sub_command_delegator) { return "..."; } else { my $detailed = $self->_shell_args_usage_string; if (length($detailed) <= 20) { return $detailed; } else { return substr($detailed,0,17) . '...'; } } } # # The following methods build allow a command to determine its # sub-commands, if there are any. # # This is for cases in which the Foo::Bar command delegates to # Foo::Bar::Baz, Foo::Bar::Buz or Foo::Bar::Doh, depending on its paramters. sub sub_command_dirs { my $class = shift; my $module = ref($class) || $class; $module =~ s/::/\//g; # multiple dirs is not working quite yet #my @paths = grep { -d $_ } map { "$_/$module" } @INC; #return @paths; $module .= '.pm'; my $path = $INC{$module}; unless ($path) { return; } $path =~ s/.pm$//; unless (-d $path) { return; } return $path; } sub sub_command_classes { my $class = shift; my @paths = $class->sub_command_dirs; return unless @paths; @paths = grep { s/\.pm$// } map { glob("$_/*") } grep { -d $_ } grep { defined($_) and length($_) } @paths; return unless @paths; my @classes = grep { ($_->is_sub_command_delegator or !$_->__meta__->is_abstract) } grep { $_ and $_->isa('Command') } map { $class->class_for_sub_command($_) } map { s/_/-/g; $_ } map { basename($_) } @paths; return @classes; } sub sub_command_names { my $class = shift; my @sub_command_classes = $class->sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; my $sub_class = join("", map { ucfirst($_) } split(/-/, $sub_command)); $sub_class = $class . "::" . $sub_class; my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } # Run the given command-line with stdout and stderr redirected to /dev/null sub system_inhibit_std_out_err { my($self,$cmdline) = @_; open my $oldout, ">&STDOUT" or die "Can't dup STDOUT: $!"; open my $olderr, ">&", \*STDERR or die "Can't dup STDERR: $!"; open(STDOUT,'>/dev/null'); open(STDERR,'>/dev/null'); my $ec = system ( $cmdline ); open STDOUT, ">&", $oldout or die "Can't dup \$oldout: $!"; open STDERR, ">&", $olderr or die "Can't dup \$olderr: $!"; return $ec; } sub parent_command_class { my $class = shift; $class = ref($class) if ref($class); my @components = split("::", $class); return if @components == 1; my $parent = join("::", @components[0..$#components-1]); return $parent if $parent->can("command_name"); return; } 1; __END__ =pod =head1 NAME Command - base class for modules implementing the command pattern =head1 SYNOPSIS use TopLevelNamespace; class TopLevelNamespace::SomeObj::Command { is => 'Command', has => [ someobj => { is => 'TopLevelNamespace::SomeObj', id_by => 'some_obj_id' }, verbose => { is => 'Boolean', is_optional => 1 }, ], }; sub execute { my $self = shift; if ($self->verbose) { print "Working on id ",$self->some_obj_id,"\n"; } my $result = $someobj->do_something(); if ($self->verbose) { print "Result was $result\n"; } return $result; } sub help_brief { return 'Call do_something on a SomeObj instance'; } sub help_synopsis { return 'cmd --some_obj_id 123 --verbose'; } sub help_detail { return 'This command performs a FooBarBaz transform on a SomObj object instance by calling its do_something method.'; } # Another part of the code my $cmd = TopLevelNamespace::SomeObj::Command->create(some_obj_id => $some_obj->id); $cmd->execute(); =head1 DESCRIPTION The Command module is a base class for creating other command modules implementing the Command Pattern. These modules can be easily reused in applications or loaded and executed dynamicaly in a command-line program. Each Command subclass represents a reusable work unit. The bulk of the module's code will likely be in the execute() method. execute() will usually take only a single argument, an instance of the Command subclass. =head1 Command-line use Creating a top-level Command module called, say TopLevelNamespace::Command, and a script called tln_cmd that looks like: #!/usr/bin/perl use TopLevelNamespace; TopLevelNamespace::Command->execute_with_shell_params_and_exit(); gives you an instant command-line tool as an interface to the hierarchy of command modules at TopLevelNamespace::Command. For example: > tln_cmd foo bar --baz 1 --qux will create an instance of TopLevelNamespace::Command::Foo::Bar (if that class exists) with params baz => 1 and qux => 1, assumming qux is a boolean property, call execute() on it, and translate the return value from execute() into the appropriate notion of a shell return value, meaning that if execute() returns true in the Perl sense, then the script returns 0 - true in the shell sense. The infrastructure takes care of turning the command line parameters into parameters for create(). Params designated as is_optional are, of course, optional and non-optional parameters that are missing will generate an error. --help is an implicit param applicable to all Command modules. It generates some hopefully useful text based on the documentation in the class definition (the 'doc' attributes you can attach to a class and properties), and the strings returned by help_detail(), help_brief() and help_synopsis(). =head1 TODO This documentation needs to be fleshed out more. There's a lot of special things you can do with Command modules that isn't mentioned here yet. =cut V2.pm100664023532023421 2467412544604516 14643 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::V2; use strict; use warnings; use UR; use Data::Dumper; use File::Basename; use Getopt::Long; use Command::View::DocMethods; use Command::Dispatch::Shell; our $VERSION = "0.44"; # UR $VERSION; our $entry_point_class; our $entry_point_bin; UR::Object::Type->define( class_name => __PACKAGE__, is => ['Command', 'Command::Common'], is_abstract => 1, subclass_description_preprocessor => 'Command::V2::_preprocess_subclass_description', attributes_have => [ is_param => { is => 'Boolean', is_optional => 1 }, is_input => { is => 'Boolean', is_optional => 1 }, is_output => { is => 'Boolean', is_optional => 1 }, shell_args_position => { is => 'Integer', is_optional => 1, doc => 'when set, this property is a positional argument when run from a shell' }, completion_handler => { is => 'MethodName', is_optional => 1, doc => 'to supply auto-completions for this parameter, call this class method' }, require_user_verify => { is => 'Boolean', is_optional => 1, doc => 'when expanding user supplied values: 0 = never verify, 1 = always verify, undef = determine automatically', }, ], has_optional => [ debug => { is => 'Boolean', doc => 'enable debug messages' }, is_executed => { is => 'Boolean' }, result => { is => 'Scalar', is_output => 1 }, original_command_line => { is => 'String', doc => 'null-byte separated list of command and arguments when run via execute_with_shell_params_and_exit'}, _total_command_count => { is => 'Integer', default => 0, is_transient => 1 }, _command_errors => { is => 'HASH', doc => 'Values can be an array ref is multiple errors occur during a command\'s execution', default => {}, is_transient => 1, }, ], ); sub _is_hidden_in_docs { return; } sub _preprocess_subclass_description { my ($class, $desc) = @_; while (my ($prop_name, $prop_desc) = each(%{ $desc->{has} })) { unless ( $prop_desc->{'is_param'} or $prop_desc->{'is_input'} or $prop_desc->{'is_transient'} or $prop_desc->{'is_calculated'}, or $prop_desc->{'is_output'} ) { $prop_desc->{'is_param'} = 1; } } return $desc; } sub _init_subclass { # Each Command subclass has an automatic wrapper around execute(). # This ensures it can be called as a class or instance method, # and that proper handling occurs around it. my $subclass_name = $_[0]; no strict; no warnings; if ($subclass_name->can('execute')) { # NOTE: manipulating %{ $subclass_name . '::' } directly causes ptkdb to segfault perl my $new_symbol = "${subclass_name}::_execute_body"; my $old_symbol = "${subclass_name}::execute"; *$new_symbol = *$old_symbol; undef *$old_symbol; } else { #print "no execute in $subclass_name\n"; } if($subclass_name->can('shortcut')) { my $new_symbol = "${subclass_name}::_shortcut_body"; my $old_symbol = "${subclass_name}::shortcut"; *$new_symbol = *$old_symbol; undef *$old_symbol; } my @p = $subclass_name->__meta__->properties(); my @e; for my $p (@p) { next if $p->property_name eq 'id'; next if $p->class_name eq __PACKAGE__; next unless $p->class_name->isa('Command'); unless ($p->is_input or $p->is_output or $p->is_param or $p->is_transient or $p->is_calculated) { my $modname = $subclass_name; $modname =~ s|::|/|g; $modname .= '.pm'; push @e, $modname . " property " . $p->property_name . " must be input, output, param, transient, or calculated!"; } } if (@e) { for (@e) { $subclass_name->error_message($_); } die "command classes like $subclass_name have properties without is_input/output/param/transient/calculated set!"; } return 1; } sub __errors__ { my ($self,@property_names) = @_; my @errors1 =($self->SUPER::__errors__); if ($self->is_executed) { return @errors1; } # for Commands which have not yet been executed, # only consider errors on inputs or params my $meta = $self->__meta__; my @errors2; ERROR: for my $e (@errors1) { for my $p ($e->properties) { my $pm = $meta->property($p); if ($pm->is_input or $pm->is_param) { push @errors2, $e; next ERROR; } } } return @errors2; } # For compatability with Command::V1 callers sub is_sub_command_delegator { return; } sub _wrapper_has { my ($class, $new_class_base) = @_; $new_class_base ||= __PACKAGE__; my $command_meta = $class->__meta__; my @properties = $command_meta->properties(); my %has; for my $property (@properties) { my %desc; next unless $property->can("is_param") and $property->can("is_input") and $property->can("is_output"); my $name = $property->property_name; next if $new_class_base->can($name); if ($property->is_param) { $desc{is_param} = 1; } elsif ($property->is_input) { $desc{is_input} = 1; } #elsif ($property->can("is_metric") and $property->is_metric) { # $desc{is_metric} = 1; #} #elsif ($property->can("is_output") and $property->is_output) { # $desc{is_output} = 1; #} else { next; } $has{$name} = \%desc; $desc{is} = $property->data_type; $desc{doc} = $property->doc; $desc{is_many} = $property->is_many; $desc{is_optional} = $property->is_optional; } return %has; } sub display_command_summary_report { my $self = shift; my $total_count = $self->_total_command_count; my %command_errors = %{$self->_command_errors}; if (keys %command_errors) { $self->status_message("\n\nErrors Summary:"); for my $key (keys %command_errors) { my $errors = $command_errors{$key}; $errors = [$errors] unless (ref($errors) and ref($errors) eq 'ARRAY'); my @errors = @{$errors}; print "$key: \n"; for my $error (@errors) { $error = $self->truncate_error_message($error); print "\t- $error\n"; } } } if ($total_count > 1) { my $error_count = scalar(keys %command_errors); $self->status_message("\n\nCommand Summary:"); $self->status_message(" Successful: " . ($total_count - $error_count)); $self->status_message(" Errors: " . $error_count); $self->status_message(" Total: " . $total_count); } } sub append_error { my $self = shift; my $key = shift || die; my $error = shift || die; my $command_errors = $self->_command_errors; push @{$command_errors->{$key}}, $error; $self->_command_errors($command_errors); return 1; } sub truncate_error_message { my $self = shift; my $error = shift || die; # truncate errors so they are actually a summary ($error) = split("\n", $error); # meant to truncate a callstack as this is meant for user/high-level $error =~ s/\ at\ \/.*//; return $error; } 1; __END__ =pod =head1 NAME Command - base class for modules implementing the command pattern =head1 SYNOPSIS use TopLevelNamespace; class TopLevelNamespace::SomeObj::Command { is => 'Command', has => [ someobj => { is => 'TopLevelNamespace::SomeObj', id_by => 'some_obj_id' }, verbose => { is => 'Boolean', is_optional => 1 }, ], }; sub execute { my $self = shift; if ($self->verbose) { print "Working on id ",$self->some_obj_id,"\n"; } my $result = $someobj->do_something(); if ($self->verbose) { print "Result was $result\n"; } return $result; } sub help_brief { return 'Call do_something on a SomeObj instance'; } sub help_synopsis { return 'cmd --some_obj_id 123 --verbose'; } sub help_detail { return 'This command performs a FooBarBaz transform on a SomObj object instance by calling its do_something method.'; } # Another part of the code my $cmd = TopLevelNamespace::SomeObj::Command->create(some_obj_id => $some_obj->id); $cmd->execute(); =head1 DESCRIPTION The Command module is a base class for creating other command modules implementing the Command Pattern. These modules can be easily reused in applications or loaded and executed dynamicaly in a command-line program. Each Command subclass represents a reusable work unit. The bulk of the module's code will likely be in the execute() method. execute() will usually take only a single argument, an instance of the Command subclass. =head1 Command-line use Creating a top-level Command module called, say TopLevelNamespace::Command, and a script called tln_cmd that looks like: #!/usr/bin/perl use TopLevelNamespace; TopLevelNamespace::Command->execute_with_shell_params_and_exit(); gives you an instant command-line tool as an interface to the hierarchy of command modules at TopLevelNamespace::Command. For example: > tln_cmd foo bar --baz 1 --qux will create an instance of TopLevelNamespace::Command::Foo::Bar (if that class exists) with params baz => 1 and qux => 1, assumming qux is a boolean property, call execute() on it, and translate the return value from execute() into the appropriate notion of a shell return value, meaning that if execute() returns true in the Perl sense, then the script returns 0 - true in the shell sense. The infrastructure takes care of turning the command line parameters into parameters for create(). Params designated as is_optional are, of course, optional and non-optional parameters that are missing will generate an error. --help is an implicit param applicable to all Command modules. It generates some hopefully useful text based on the documentation in the class definition (the 'doc' attributes you can attach to a class and properties), and the strings returned by help_detail(), help_brief() and help_synopsis(). =head1 TODO This documentation needs to be fleshed out more. There's a lot of special things you can do with Command modules that isn't mentioned here yet. =cut V2Deprecated.pm100664023532023421 1721312544604516 16613 0ustar00abrummetgsc000000000000UR-0.44/lib/Commandpackage Command::V2; # additional methods to dispatch from a command-line use strict; use warnings; sub sorted_sub_command_classes { no warnings; my @c = shift->sub_command_classes; my @commands_with_position = map { [ $_->sub_command_sort_position, $_ ] } @c; return map { $_->[1] } sort { ($a->[0] <=> $b->[0]) || ($a->[0] cmp $b->[0]) } @commands_with_position; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub help_sub_commands { my $class = shift; my %params = @_; my $command_name_method = 'command_name_brief'; #my $command_name_method = ($params{brief} ? 'command_name_brief' : 'command_name'); my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @categories; for my $sub_command_class (@sub_command_classes) { my $category = $sub_command_class->sub_command_category; $category = '' if not defined $category; next if $sub_command_class->_is_hidden_in_docs(); my $sub_commands_within_category = $categories{$category}; unless ($sub_commands_within_category) { if (defined $category and length $category) { push @categories, $category; } else { unshift @categories,''; } $sub_commands_within_category = $categories{$category} = []; } push @$sub_commands_within_category,$sub_command_class; } no warnings; local $Text::Wrap::columns = 60; my $full_text = ''; my @full_data; for my $category (@categories) { my $sub_commands_within_this_category = $categories{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, $_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= Term::ANSIColor::colored($row->[$c], $colors[$c]), $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } #$DB::single = 1; return $text; } sub sub_command_dirs { my $class = shift; my $subdir = ref($class) || $class; $subdir =~ s|::|\/|g; my @dirs = grep { -d $_ } map { $_ . '/' . $subdir } @INC; return @dirs; } sub sub_command_classes { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return values %$mapping; } sub _build_sub_command_mapping { my $class = shift; $class = ref($class) || $class; my $mapping; do { no strict 'refs'; $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; }; unless (defined $mapping) { my $subdir = $class; $subdir =~ s|::|\/|g; for my $lib (@INC) { my $subdir_full_path = $lib . '/' . $subdir; next unless -d $subdir_full_path; my @files = glob($subdir_full_path . '/*'); next unless @files; for my $file (@files) { my $basename = basename($file); $basename =~ s/.pm$//; my $sub_command_class_name = $class . '::' . $basename; my $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); unless ($sub_command_class_meta) { local $SIG{__DIE__}; local $SIG{__WARN__}; eval "use $sub_command_class_name"; } $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); next unless $sub_command_class_name->isa("Command"); next if $sub_command_class_meta->is_abstract; my $name = $class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } } return $mapping; } sub sub_command_names { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return keys %$mapping; } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; my $sub_class = join("", map { ucfirst($_) } split(/-/, $sub_command)); $sub_class = $class . "::" . $sub_class; my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } 1; 1; DocMethods.pm100664023532023421 4627312544604516 17316 0ustar00abrummetgsc000000000000UR-0.44/lib/Command/Viewpackage Command::V2; # additional methods to produce documentation, TODO: turn into a real view use strict; use warnings; use Term::ANSIColor qw(); use Pod::Simple::Text; require Text::Wrap; # This is changed with "local" where used in some places $Text::Wrap::columns = 100; # Required for color output eval { binmode STDOUT, ":utf8"; binmode STDERR, ":utf8"; }; sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } return "no description!!!: define 'doc' in the class definition for " . $self->class; } } sub help_synopsis { my $self = shift; return ''; } sub help_detail { my $self = shift; return "!!! define help_detail() in module " . ref($self) || $self . "!"; } sub sub_command_category { return; } sub sub_command_sort_position { # override to do something besides alpha sorting by name return '9999999999 ' . $_[0]->command_name_brief; } # LEGACY: poorly named sub help_usage_command_pod { return shift->doc_manual(@_); } # LEGACY: poorly named sub help_usage_complete_text { shift->doc_help(@_) } sub doc_help { my $self = shift; my $command_name = $self->command_name; my $text; my $extra_help = ''; my @extra_help = $self->_additional_help_sections; while (@extra_help) { my $title = shift @extra_help || ''; my $content = shift @extra_help || ''; $extra_help .= sprintf( "%s\n\n%s\n", Term::ANSIColor::colored($title, 'underline'), _pod2txt($content) ), } # standard: update this to do the old --help format my $synopsis = $self->help_synopsis; my $required_inputs = $self->help_options(is_optional => 0, is_input => 1); my $required_outputs = $self->help_options(is_optional => 0, is_output => 1); my $required_params = $self->help_options(is_optional => 0, is_param => 1); my $optional_inputs = $self->help_options(is_optional => 1, is_input => 1); my $optional_outputs = $self->help_options(is_optional => 1, is_output => 1); my $optional_params = $self->help_options(is_optional => 1, is_param => 1); my @parts; push @parts, Term::ANSIColor::colored('USAGE', 'underline'); push @parts, Text::Wrap::wrap( ' ', ' ', Term::ANSIColor::colored($self->command_name, 'bold'), $self->_shell_args_usage_string || '', ); push @parts, ( $synopsis ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SYNOPSIS", 'underline'), $synopsis) : '' ); push @parts, ( $required_inputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED INPUTS", 'underline'), $required_inputs) : '' ); push @parts, ( $required_params ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED PARAMS", 'underline'), $required_params) : '' ); push @parts, ( $optional_inputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL INPUTS", 'underline'), $optional_inputs) : '' ); push @parts, ( $optional_params ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL PARAMS", 'underline'), $optional_params) : '' ); push @parts, ( $required_outputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED OUTPUTS", 'underline'), $required_outputs) : '' ); push @parts, ( $optional_outputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL OUTPUTS", 'underline'), $optional_outputs) : '' ); push @parts, sprintf( "%s\n%s\n", Term::ANSIColor::colored("DESCRIPTION", 'underline'), _pod2txt($self->help_detail || '') ); push @parts, ( $extra_help ? $extra_help : '' ); $text = sprintf( "\n%s\n%s\n\n%s%s%s%s%s%s%s%s%s\n", @parts ); return $text; } sub parent_command_class { my $class = shift; $class = ref($class) if ref($class); my @components = split("::", $class); return if @components == 1; my $parent = join("::", @components[0..$#components-1]); return $parent if $parent->can("command_name"); return; } sub doc_sections { my $self = shift; my @sections; my $command_name = $self->command_name; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $help_brief = $self->help_brief; my $datetime = $self->__context__->now; my ($date,$time) = split(' ',$datetime); push(@sections, UR::Doc::Section->create( title => "NAME", content => "$command_name" . ($help_brief ? " - $help_brief" : ""), format => "pod", )); push(@sections, UR::Doc::Section->create( title => "VERSION", content => "This document " # separated to trick the version updater . "describes $command_name " . ($version ? "version $version " : "") . "($date at $time)", format => "pod", )); my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; if ($synopsis) { push(@sections, UR::Doc::Section->create( title => "SYNOPSIS", content => $synopsis, format => 'pod' )); } my $required_args = $self->help_options(is_optional => 0, format => "pod"); if ($required_args) { push(@sections, UR::Doc::Section->create( title => "REQUIRED ARGUMENTS", content => "=over\n\n$required_args\n\n=back\n\n", format => 'pod' )); } my $optional_args = $self->help_options(is_optional => 1, format => "pod"); if ($optional_args) { push(@sections, UR::Doc::Section->create( title => "OPTIONAL ARGUMENTS", content => "=over\n\n$optional_args\n\n=back\n\n", format => 'pod' )); } my $manual = $self->_doc_manual_body || $self->help_detail; push(@sections, UR::Doc::Section->create( title => "DESCRIPTION", content => $manual, format => 'pod', )); my @extra_help = $self->_additional_help_sections; while (@extra_help) { my $title = shift @extra_help || ''; my $content = shift @extra_help || ''; push (@sections, UR::Doc::Section->create( title => $title, content => $content, format => 'pod' )); } if ($self->can("doc_sub_commands")) { my $sub_commands = $self->doc_sub_commands(brief => 1); if ($sub_commands) { push(@sections, UR::Doc::Section->create( title => "SUB-COMMANDS", content => $sub_commands, format => "pod", )); } } my @footer_section_methods = ( 'LICENSE' => '_doc_license', 'AUTHORS' => '_doc_authors', 'CREDITS' => '_doc_credits', 'BUGS' => '_doc_bugs', 'SEE ALSO' => '_doc_see_also' ); while (@footer_section_methods) { my $header = shift @footer_section_methods; my $method = shift @footer_section_methods; my @txt = $self->$method; next if (@txt == 0 or (@txt == 1 and not $txt[0])); my $content; if (@txt == 1) { $content = $txt[0]; } else { $content = join("\n", @txt); } push(@sections, UR::Doc::Section->create( title => $header, content => $content, format => "pod", )); } return @sections; } sub doc_sub_commands { my $self = shift; return; } sub doc_manual { my $self = shift; my $pod = $self->_doc_name_version; my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0, format => "pod"); my $optional_args = $self->help_options(is_optional => 1, format => "pod"); $pod .= ( $synopsis ? "=head1 SYNOPSIS\n\n" . $synopsis . "\n\n" : '' ) . ( $required_args ? "=head1 REQUIRED ARGUMENTS\n\n=over\n\n" . $required_args . "\n\n=back\n\n" : '' ) . ( $optional_args ? "=head1 OPTIONAL ARGUMENTS\n\n=over\n\n" . $optional_args . "\n\n=back\n\n" : '' ); my $manual = $self->_doc_manual_body; my $help = $self->help_detail; if ($manual or $help) { $pod .= "=head1 DESCRIPTION:\n\n"; my $txt = $manual || $help; if ($txt =~ /^\=/) { # pure POD $pod .= $manual; } else { $txt =~ s/\n/\n\n/g; $pod .= $txt; #$pod .= join('', map { " $_\n" } split ("\n",$txt)) . "\n"; } } $pod .= $self->_doc_footer(); $pod .= "\n\n=cut\n\n"; return "\n$pod"; } sub _doc_name_version { my $self = shift; my $command_name = $self->command_name; my $pod; # standard: update this to do the old --help format my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $help_brief = $self->help_brief; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $datetime = $self->__context__->now; my ($date,$time) = split(' ',$datetime); $pod = "\n=pod" . "\n\n=head1 NAME" . "\n\n" . $self->command_name . ($help_brief ? " - " . $self->help_brief : '') . "\n\n"; $pod .= "\n\n=head1 VERSION" . "\n\n" . "This document " # separated to trick the version updater . "describes " . $self->command_name; if ($version) { $pod .= " version " . $version . " ($date at $time).\n\n"; } else { $pod .= " ($date at $time)\n\n"; } return $pod; } sub _doc_manual_body { return ''; } sub help_header { my $class = shift; return sprintf("%s - %-80s\n", $class->command_name ,$class->help_brief ) } sub help_options { my $self = shift; my %params = @_; my $format = delete $params{format}; my @property_meta = $self->_shell_args_property_meta(%params); my @data; my $max_name_length = 0; for my $property_meta (@property_meta) { my $param_name = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $param_name = uc($param_name); } #$param_name = "--$param_name"; my $doc = $property_meta->doc; my $valid_values = $property_meta->valid_values; my $example_values = $property_meta->example_values; unless ($doc) { # Maybe a parent class has documentation for this property eval { foreach my $ancestor_class_meta ( $property_meta->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property_meta->property_name); if ($ancestor_property_meta and $doc = $ancestor_property_meta->doc) { last; } } }; } if (!$doc) { if (!$valid_values) { $doc = "(undocumented)"; } else { $doc = ''; } } if ($valid_values) { $doc .= "\nvalid values:\n"; for my $v (@$valid_values) { $doc .= " " . $v . "\n"; $max_name_length = length($v)+2 if $max_name_length < length($v)+2; } chomp $doc; } if ($example_values && @$example_values) { $doc .= "\nexample" . (@$example_values > 1 and 's') . ":\n"; $doc .= join(', ', map { ref($_) ? Data::Dumper->new([$_])->Terse(1)->Dump() : $_ } @$example_values ); chomp($doc); } $max_name_length = length($param_name) if $max_name_length < length($param_name); my $param_type = $property_meta->data_type || ''; if (defined($param_type) and $param_type !~ m/::/) { $param_type = ucfirst(lc($param_type)); } my $default_value; if (defined($default_value = $property_meta->default_value) || defined(my $calculated_default = $property_meta->calculated_default) ) { unless (defined $default_value) { $default_value = $calculated_default->() } if ($param_type eq 'Boolean') { $default_value = $default_value ? "'true'" : "'false' (--no$param_name)"; } elsif ($property_meta->is_many && ref($default_value) eq 'ARRAY') { if (@$default_value) { $default_value = "('" . join("','",@$default_value) . "')"; } else { $default_value = "()"; } } else { $default_value = "'$default_value'"; } $default_value = "\nDefault value $default_value if not specified"; } push @data, [$param_name, $param_type, $doc, $default_value]; if ($param_type eq 'Boolean') { push @data, ['no'.$param_name, $param_type, "Make $param_name 'false'" ]; } } my $text = ''; for my $row (@data) { if (defined($format) and $format eq 'pod') { $text .= "\n=item " . $row->[0] . ($row->[1]? ' I<' . $row->[1] . '>' : '') . "\n\n" . $row->[2] . "\n". ($row->[3]? $row->[3] . "\n" : ''); } elsif (defined($format) and $format eq 'html') { $text .= "\n\t
" . $row->[0] . ($row->[1]? ' ' . $row->[1] . '' : '') . "
" . $row->[2] . ($row->[3]? "
" . $row->[3] : '') . "
\n"; } else { $text .= sprintf( " %s\n%s\n", Term::ANSIColor::colored($row->[0], 'bold'), # . " " . $row->[1], Text::Wrap::wrap( " ", # 1st line indent, " ", # all other lines indent, $row->[2], $row->[3] || '', ), ); } } return $text; } sub _doc_footer { my $self = shift; my $pod = ''; my @method_header_map = ( 'LICENSE' => '_doc_license', 'AUTHORS' => '_doc_authors', 'CREDITS' => '_doc_credits', 'BUGS' => '_doc_bugs', 'SEE ALSO' => '_doc_see_also' ); while (@method_header_map) { my $header = shift @method_header_map; my $method = shift @method_header_map; my @txt = $self->$method; next if (@txt == 0 or (@txt == 1 and not $txt[0])); if (@txt == 1) { my @lines = split("\n",$txt[0]); $pod .= "=head1 $header\n\n" . join(" \n", @lines) . "\n\n"; } else { $pod .= "=head1 $header\n\n" . join("\n ",@txt); $pod .= "\n\n"; } } return $pod; } sub _doc_license { return ''; } sub _doc_authors { return (); } sub _doc_credits { return ''; } sub _doc_bugs { return ''; } sub _doc_see_also { return (); } sub _shell_args_usage_string { my $self = shift; return eval { if ( $self->isa('Command::Tree') ) { return '...'; } elsif ($self->can("_execute_body") eq __PACKAGE__->can("_execute_body")) { return '(no execute!)'; } elsif ($self->__meta__->is_abstract) { return '(no sub commands!)'; } else { return join( " ", map { $self->_shell_arg_usage_string_from_property_meta($_) } $self->_shell_args_property_meta() ); } }; } sub _shell_args_usage_string_abbreviated { my $self = shift; my $detailed = $self->_shell_args_usage_string; if (length($detailed) <= 20) { return $detailed; } else { return substr($detailed,0,17) . '...'; } } sub sub_command_mapping { my ($self, $class) = @_; return if !$class; no strict 'refs'; my $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; if (ref($mapping) eq 'HASH') { return $mapping; } else { return; } }; sub command_name { my $self = shift; my $class = ref($self) || $self; my $prepend = ''; # There can be a hash in the command entry point class that maps # root level tools to classes so they can be in a different location # ...this bit of code considers that misdirection: my $entry_point_class = $Command::entry_point_class; my $mapping = $self->sub_command_mapping($entry_point_class); for my $k (%$mapping) { my $v = $mapping->{$k}; if ($v && $v eq $class) { my @words = grep { $_ ne 'Command' } split(/::/,$class); return join(' ', $self->_command_name_for_class_word($words[0]), $k); } } if (defined($entry_point_class) and $class =~ /^($entry_point_class)(::.+|)$/) { $prepend = $Command::entry_point_bin; $class = $2; if ($class =~ s/^:://) { $prepend .= ' '; } } my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } @words); return $prepend . $n; } sub command_name_brief { my $self = shift; my $class = ref($self) || $self; my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } $words[-1]); return $n; } sub color_command_name { my $text = shift; my $colored_text = []; my @COLOR_TEMPLATES = ('red', 'bold red', 'magenta', 'bold magenta'); my @parts = split(/\s+/, $text); for(my $i = 0 ; $i < @parts ; $i++ ){ push @$colored_text, ($i < @COLOR_TEMPLATES) ? Term::ANSIColor::colored($parts[$i], $COLOR_TEMPLATES[$i]) : $parts[$i]; } return join(' ', @$colored_text); } sub _base_command_class_and_extension { my $self = shift; my $class = ref($self) || $self; return ($class =~ /^(.*)::([^\:]+)$/); } sub _command_name_for_class_word { my $self = shift; my $s = shift; $s =~ s/_/-/g; $s =~ s/^([A-Z])/\L$1/; # ignore first capital because that is assumed $s =~ s/([A-Z])/-$1/g; # all other capitals prepend a dash $s =~ s/([a-zA-Z])([0-9])/$1$2/g; # treat number as begining word $s = lc($s); return $s; } sub _pod2txt { my $txt = shift; my $output = ''; my $parser = Pod::Simple::Text->new; $parser->no_errata_section(1); $parser->output_string($output); $parser->parse_string_document("=pod\n\n$txt"); return $output; } sub _additional_help_sections { return; } 1; callcount.pm100664023532023421 514512544604516 15771 0ustar00abrummetgsc000000000000UR-0.44/lib/Devel package Devel::callsfrom; use Data::Dumper; # From perldoc perlvar # Debugger flags, so you can see what we turn on below. # # 0x01 Debug subroutine enter/exit. # # 0x02 Line-by-line debugging. # # 0x04 Switch off optimizations. # # 0x08 Preserve more data for future interactive inspections. # # 0x10 Keep info about source lines on which a subroutine is defined. # # 0x20 Start with single-step on. # # 0x40 Use subroutine address instead of name when reporting. # # 0x80 Report "goto &subroutine" as well. # # 0x100 Provide informative "file" names for evals based on the place they were com- # piled. # # 0x200 Provide informative names to anonymous subroutines based on the place they # were compiled. # # 0x400 Debug assertion subroutines enter/exit. # BEGIN { $^P |= (0x01 | 0x80 | 0x100 | 0x200); }; #BEGIN { $^P |= (0x004 | 0x100 ); }; sub import { } package DB; # Any debugger needs to have a sub DB. It doesn't need to do anything. sub DB{}; # We want to track how deep our subroutines go our $CALL_DEPTH = 0; our %CALLED; our $CALL_WATCH = $ENV{CALL_WATCH}; sub sub { local $DB::CALL_DEPTH = $DB::CALL_DEPTH+1; no strict; no warnings; my @c0 = caller(0); my @c1 = caller(-1); my ($pkg,$file,$line) = @c1; my $csub = $c0[3] || '-'; my $caller = join(",", $file,$line,$pkg,$csub); print STDERR ((' ' x $DB::CALL_DEPTH) . $DB::sub{$DB::sub} . ' > ' . $DB::sub . "(@_) : " . $caller . "\n") if $CALL_WATCH; $DB::CALLED{$DB::sub}{$caller}++; &{$DB::sub}; } END { use strict; use warnings; my %counts; for my $sub (keys %DB::sub) { my $cases = $DB::CALLED{$sub}; my @callers = keys %$cases; my $call_count = scalar(@callers); $counts{$call_count}{$sub} = $cases; } my @counts = keys %counts; my $call_min = $ENV{CALL_MIN}; if (defined $call_min) { @counts = grep { $_ >= $call_min } @counts; } my $call_max = $ENV{CALL_MAX}; if (defined $call_max) { @counts = grep { $_ <= $call_max } @counts; } my $fh; if (my $fname = $ENV{CALL_COUNT_OUTFILE}) { open($fh,">$fname"); unless ($fh) { die "failed to open outfile for call count for $0!" }; } else { open($fh,">$0.callcount"); $fh or die "failed to open output file $0.callcount: $!"; } for my $c (sort { $a <=> $b } @counts) { my $subs = $counts{$c}; for my $sub (sort keys %$subs) { my $cases = $subs->{$sub}; my @calls = sort keys %$cases; print $fh join("\t",$c, $sub,$DB::sub{$sub},@calls),"\n"; } } } 1; UR.pm100664023532023421 10227112544604516 13332 0ustar00abrummetgsc000000000000UR-0.44/libpackage UR; # The UR module is itself a "UR::Namespace", besides being the root # module which bootstraps the system. The class definition itself # is made at the bottom of the file. use strict; use warnings FATAL => 'all'; # Set the version at compile time, since some other modules borrow it. our $VERSION = "0.44"; # UR $VERSION # Ensure we get detailed errors while starting up. # This is disabled at the bottom of the module. use Carp; $SIG{__DIE__} = \&Carp::confess; # Ensure that, if the application changes directory, we do not # change where we load modules while running. use Cwd; my @PERL5LIB = ($ENV{PERL5LIB} ? split(':', $ENV{PERL5LIB}) : ()); for my $dir (@INC, @PERL5LIB) { next unless -d $dir; $dir = Cwd::abs_path($dir) || $dir; } $ENV{PERL5LIB} = join(':', @PERL5LIB); # Also need to fix modules that were already loaded, so that when # a namespace is loaded the path will not change out from # underneath it. for my $module (keys %INC) { $INC{$module} = Cwd::abs_path($INC{$module}); } # UR supports several environment variables, found under UR/ENV # Any UR_* variable which is set but does NOT corresponde to a module found will cause an exit # (a hedge against typos such as UR_DBI_NO_COMMMMIT=1 leading to unexpected behavior) for my $e (keys %ENV) { next unless substr($e,0,3) eq 'UR_'; eval "use UR::Env::$e"; if ($@) { my $path = __FILE__; $path =~ s/.pm$//; my @files = glob($path . '/Env/*'); my @vars = map { /UR\/Env\/(.*).pm/; $1 } @files; print STDERR "Environment variable $e set to $ENV{$e} but there were errors using UR::Env::$e:\n" . "Available variables:\n\t" . join("\n\t",@vars) . "\n"; exit 1; } } # These two dump info about used modules and libraries at program exit. END { if ($ENV{UR_USED_LIBS}) { print STDERR "Used library include paths (\@INC):\n"; for my $lib (@INC) { print STDERR "$lib\n"; } print STDERR "\n"; } if ($ENV{UR_USED_MODS}) { print STDERR "Used modules and paths (\%INC):\n"; for my $mod (sort keys %INC) { if ($ENV{UR_USED_MODS} > 1) { print STDERR "$mod => $INC{$mod}\n"; } else { print STDERR "$mod\n"; } } print STDERR "\n"; } if ($ENV{UR_DBI_SUMMARIZE_SQL}) { UR::DBI::print_sql_summary(); } } #Class::AutoloadCAN must be used before Class::Autouse, or the can methods will break in confusing ways. use Class::AutoloadCAN; use Class::Autouse; BEGIN { my $v = $Class::Autouse::VERSION; unless (($v =~ /^\d+\.?\d*$/ && $v >= 2.0) or $v eq '1.99_02' or $v eq '1.99_04') { die "UR requires Class::Autouse 2.0 or greater (or 1.99_02 or 1.99_04)!!"; } }; # Regular deps use Date::Format; # # Because UR modules execute code when compiling to define their classes, # and require each other for that code to execute, there are bootstrapping # problems. # # Everything which is part of the core framework "requires" UR # which, of course, executes AFTER it has compiled its SUBS, # but BEFORE it defines its class. # # Everything which _uses_ the core of the framework "uses" its namespace, # either the specific top-level namespace module, or "UR" itself for components/extensions. # require UR::Exit; require UR::Util; require UR::DBI::Report; # this is used by UR::DBI require UR::DBI; # this needs a new name, and need only be used by UR::DataSource::RDBMS require UR::ModuleBase; # this should be switched to a role require UR::ModuleConfig; # used by ::Time, and also ::Lock ::Daemon require UR::Object::Iterator; require UR::Context::AutoUnloadPool; require UR::DeletedRef; require UR::Object; require UR::Object::Type; require UR::Object::Ghost; require UR::Object::Property; require UR::Observer; require UR::BoolExpr::Util; require UR::BoolExpr; # has meta require UR::BoolExpr::Template; # has meta require UR::BoolExpr::Template::PropertyComparison; # has meta require UR::BoolExpr::Template::Composite; # has meta require UR::BoolExpr::Template::And; # has meta require UR::BoolExpr::Template::Or; # has meta require UR::Object::Index; # # Define core metadata. # # This is done outside of the actual modules since the define() method # uses all of the modules themselves to do its work. # UR::Object::Type->define( class_name => 'UR::Object', is => [], # the default is to inherit from UR::Object, which is circular, so we explicitly say nothing is_abstract => 1, composite_id_separator => "\t", id_by => [ id => { is => 'Scalar', doc => 'unique identifier' } ], id_generator => '-urinternal', ); UR::Object::Type->define( class_name => "UR::Object::Index", id_by => ['indexed_class_name','indexed_property_string'], has => ['indexed_class_name','indexed_property_string'], is_transactional => 0, ); UR::Object::Type->define( class_name => 'UR::Object::Ghost', is_abstract => 1, ); UR::Object::Type->define( class_name => 'UR::Entity', extends => ['UR::Object'], is_abstract => 1, ); UR::Object::Type->define( class_name => 'UR::Entity::Ghost', extends => ['UR::Object::Ghost'], is_abstract => 1, ); # MORE METADATA CLASSES # For bootstrapping reasons, the properties with default values also need to be listed in # %class_property_defaults defined in UR::Object::Type::Initializer. If you make changes # to default values, please keep these in sync. UR::Object::Type->define( class_name => 'UR::Object::Type', doc => 'class/type meta-objects for UR', id_by => 'class_name', sub_classification_method_name => '_resolve_meta_class_name', is_abstract => 1, has => [ class_name => { is => 'Text', len => 256, is_optional => 1, doc => 'the name for the class described' }, properties => { is_many => 1, # this is calculated instead of a regular relationship # so we can do appropriate inheritance filtering. # We need an isa operator and its converse # in order to be fully declarative internally here calculate => 'shift->_properties(@_);', doc => 'property meta-objects for the class' }, id_properties => { is_many => 1, calculate => q( grep { defined $_->is_id } shift->_properties(@_) ), doc => 'meta-objects for the ID properties of the class' }, doc => { is => 'Text', len => 1024, is_optional => 1, doc => 'a one-line description of the class/type' }, is_abstract => { is => 'Boolean', default_value => 0, doc => 'abstract classes must be subclassified into a concreate class at create/load time' }, is_final => { is => 'Boolean', default_value => 0, doc => 'further subclassification is prohibited on final classes' }, is_transactional => { is => 'Boolean', default_value => 1, is_optional => 1, doc => 'non-transactional objects are left out of in-memory transactions' }, is_singleton => { is => 'Boolean', default_value => 0, doc => 'singleton classes have only one instance, or have each instance fall into a distinct subclass' }, namespace => { is => 'Text', len => 256, is_optional => 1, doc => 'the first "word" in the class name, which points to a UR::Namespace' }, schema_name => { is => 'Text', len => 256, is_optional => 1, doc => 'an arbitrary grouping for classes for which instances share a common storage system' }, data_source_id => { is => 'Text', len => 256, is_optional => 1, doc => 'for classes which persist beyond their current process, the identifier for their storage manager' }, #data_source_meta => { is => 'UR::DataSource', id_by => 'data_source_id', is_optional => 1, }, generated => { is => 'Boolean', is_transient => 1, default_value => 0, doc => 'an internal flag set when the class meta has fabricated accessors and methods in the class namespace' }, meta_class_name => { is => 'Text', doc => 'even meta-classess have a meta-class' }, composite_id_separator => { is => 'Text', len => 2 , default_value => "\t", is_optional => 1, doc => 'for classes whose objects have a multi-value "id", this overrides using a "\t" to compose/decompose' }, valid_signals => { is => 'ARRAY', is_optional => 1, doc => 'List of non-standard signal names observers can bind to ' }, # details used by the managment of the "real" entity outside of the app (persistence) table_name => { is => 'Text', len => undef, is_optional => 1, doc => 'for classes with a data source, this specifies the table or equivalent data structure which holds instances' }, select_hint => { is => 'Text', len => 1024 , is_optional => 1, doc => 'used to optimize access to underlying storage (database specific)' }, join_hint => { is => 'Text', len => 1024 , is_optional => 1, doc => 'used to optimize access to underlying storage when this class is part of a join (database specific)' }, id_generator => { is => 'Text', len => 256, is_optional => 1, doc => 'override the default choice for generating new object IDs' }, # different ways of handling subclassing at object load time subclassify_by => { is => 'Text', len => 256, is_optional => 1, doc => 'when set, the method specified will return the name of a specific subclass into which the object should go' }, subclass_description_preprocessor => { is => 'MethodName', len => 255, is_optional => 1, doc => 'a method which should pre-process the class description of sub-classes before construction' }, sub_classification_method_name => { is => 'Text', len => 256, is_optional => 1, doc => 'like subclassify_by, but examines whole objects not a single property' }, use_parallel_versions => { is => 'Boolean', is_optional => 1, default_value => 0, doc => 'inheriting from the is class will redirect to a ::V? module implemeting a specific version' }, # obsolete/internal type_name => { is => 'Text', len => 256, is_deprecated => 1, is_optional => 1 }, er_role => { is => 'Text', len => 256, is_optional => 1, default_value => 'entity' }, source => { is => 'Text', len => 256 , default_value => 'data dictionary', is_optional => 1 }, # This is obsolete and should be removed later sub_classification_meta_class_name => { is => 'Text', len => 1024 , is_optional => 1, doc => 'obsolete' }, first_sub_classification_method_name => { is => 'Text', len => 256, is_optional => 1, doc => 'cached value to handle a complex inheritance hierarchy with storage at some levels but not others' }, ### Relationships with the other meta-classes (used internally) ### # UR::Namespaces are singletons referenced through their name namespace_meta => { is => 'UR::Namespace', id_by => 'namespace' }, is => { is => 'ARRAY', is_mutable => 0, doc => 'List of the parent class names' }, # linking to the direct parents, and the complete ancestry parent_class_metas => { is => 'UR::Object::Type', id_by => 'is', doc => 'The list of UR::Object::Type objects for the classes that are direct parents of this class' },#, is_many => 1 }, parent_class_names => { via => 'parent_class_metas', to => 'class_name', is_many => 1 }, parent_meta_class_names => { via => 'parent_class_metas', to => 'meta_class_name', is_many => 1 }, ancestry_meta_class_names => { via => 'ancestry_class_metas', to => 'meta_class_name', is_many => 1 }, ancestry_class_metas => { is => 'UR::Object::Type', id_by => 'is', where => [-recurse => [class_name => 'is']], doc => 'Climb the ancestry tree and return the class objects for all of them' }, ancestry_class_names => { via => 'ancestry_class_metas', to => 'class_name', is_many => 1 }, # This one isn't useful on its own, but is used to build the all_* accessors below all_class_metas => { is => 'UR::Object::Type', calculate => 'return ($self, $self->ancestry_class_metas)' }, # Properties defined on this class, parent classes, etc. # There's also a property_meta_by_name() method defined in the class direct_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', is_many => 1 }, direct_property_names => { via => 'direct_property_metas', to => 'property_name', is_many => 1 }, direct_id_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', where => [ 'is_id true' => 1, -order_by => 'is_id' ], is_many => 1 }, direct_id_property_names => { via => 'direct_id_property_metas', to => 'property_name', is_many => 1 }, ancestry_property_metas => { via => 'ancestry_class_metas', to => 'direct_property_metas', is_many => 1 }, ancestry_property_names => { via => 'ancestry_class_metas', to => 'direct_property_names', is_many => 1 }, ancestry_id_property_metas => { via => 'ancestry_class_metas', to => 'direct_id_property_metas', is_many => 1 }, ancestry_id_property_names => { via => 'ancestry_id_property_metas', to => 'property_name', is_many => 1 }, all_property_metas => { via => 'all_class_metas', to => 'direct_property_metas', is_many => 1 }, all_property_names => { via => 'all_property_metas', to => 'property_name', is_many => 1 }, all_id_property_metas => { via => 'all_class_metas', to => 'direct_id_property_metas', is_many => 1 }, all_id_property_names => { via => 'all_id_property_metas', to => 'property_name', is_many => 1 }, direct_id_by_property_metas => { via => 'direct_property_metas', to => '__self__', where => ['id_by true' => 1], is_many => 1, doc => "Properties with 'id_by' metadata, ie. direct object accessor properties" } , all_id_by_property_metas => { via => 'all_class_metas', to => 'direct_id_by_property_metas', is_many => 1}, direct_reverse_as_property_metas => { via => 'direct_property_metas', to => '__self__', where => ['reverse_as true' => 1], is_many => 1, doc => "Properties with 'reverse_as' metadata, ie. indirect object accessor properties" }, all_reverse_as_property_metas => { via => 'all_class_metas', to => 'direct_reverse_as_property_metas', is_many => 1}, # Datasource related stuff direct_column_names => { via => 'direct_property_metas', to => 'column_name', is_many => 1, where => [column_name => { operator => 'true' }] }, direct_id_column_names => { via => 'direct_id_property_metas', to => 'column_name', is_many => 1, where => [column_name => { operator => 'true'}] }, ancestry_column_names => { via => 'ancestry_class_metas', to => 'direct_column_names', is_many => 1 }, ancestry_id_column_names => { via => 'ancestry_class_metas', to => 'direct_id_column_names', is_many => 1 }, # Are these *columnless* properties actually necessary? The user could just use direct_property_metas(column_name => undef) direct_columnless_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', where => [column_name => undef], is_many => 1 }, direct_columnless_property_names => { via => 'direct_columnless_property_metas', to => 'property_name', is_many => 1 }, ancestry_columnless_property_metas => { via => 'ancestry_class_metas', to => 'direct_columnless_property_metas', is_many => 1 }, ancestry_columnless_property_names => { via => 'ancestry_columnless_property_metas', to => 'property_name', is_many => 1 }, ancestry_table_names => { via => 'ancestry_class_metas', to => 'table_name', is_many => 1 }, all_table_names => { via => 'all_class_metas', to => 'table_name', is_many => 1 }, all_column_names => { via => 'all_class_metas', to => 'direct_column_names', is_many => 1 }, all_id_column_names => { via => 'all_class_metas', to => 'direct_id_column_names', is_many => 1 }, all_columnless_property_metas => { via => 'all_class_metas', to => 'direct_columnless_property_metas', is_many => 1 }, all_columnless_property_names => { via => 'all_class_metas', to => 'direct_columnless_property_names', is_many => 1 }, ], ); UR::Object::Type->define( class_name => 'UR::Object::Property', id_properties => [ class_name => { is => 'Text', len => 256 }, property_name => { is => 'Text', len => 256 }, ], has_optional => [ property_type => { is => 'Text', len => 256 , is_optional => 1}, column_name => { is => 'Text', len => 256, is_optional => 1 }, data_length => { is => 'Text', len => 32, is_optional => 1 }, data_type => { is => 'Text', len => 256, is_optional => 1 }, calculated_default => { is_optional => 1 }, default_value => { is_optional => 1 }, valid_values => { is => 'ARRAY', is_optional => 1, }, example_values => { is => 'ARRAY', is_optional => 1, doc => 'example valid values; used to generate help text for Commands' }, doc => { is => 'Text', len => 1000, is_optional => 1 }, is_id => { is => 'Integer', default_value => undef, doc => 'denotes this is an ID property of the class, and ranks them' }, is_optional => { is => 'Boolean' , default_value => 0}, is_transient => { is => 'Boolean' , default_value => 0}, is_constant => { is => 'Boolean' , default_value => 0}, # never changes is_mutable => { is => 'Boolean' , default_value => 1}, # can be changed explicitly via accessor (cannot be constant) is_volatile => { is => 'Boolean' , default_value => 0}, # changes w/o a signal: (cannot be constant or transactional) is_classwide => { is => 'Boolean' , default_value => 0}, is_delegated => { is => 'Boolean' , default_value => 0}, is_calculated => { is => 'Boolean' , default_value => 0}, is_transactional => { is => 'Boolean' , default_value => 1}, # STM works on these, and the object can possibly save outside the app is_abstract => { is => 'Boolean' , default_value => 0}, is_concrete => { is => 'Boolean' , default_value => 1}, is_final => { is => 'Boolean' , default_value => 0}, is_many => { is => 'Boolean' , default_value => 0}, is_aggregate => { is => 'Boolean' , default_value => 0}, is_deprecated => { is => 'Boolean', default_value => 0}, is_numeric => { calculate_from => ['data_type'], }, id_by => { is => 'ARRAY', is_optional => 1}, id_class_by => { is => 'Text', is_optional => 1}, is_undocumented => { is => 'Boolean', is_optional => 1, doc => 'do not show in documentation to users' }, doc_position => { is => 'Number', is_optional => 1, doc => 'override the sort position within documentation' }, access_as => { is => 'Text', is_optional => 1, doc => 'when id_class_by is set, and this is set to "auto", primitives will return as their ID instead of boxed' }, order_by => { is => 'ARRAY', is_optional => 1}, specify_by => { is => 'Text', is_optional => 1}, reverse_as => { is => 'ARRAY', is_optional => 1 }, implied_by => { is => 'Text' , is_optional => 1}, via => { is => 'Text' , is_optional => 1 }, to => { is => 'Text' , is_optional => 1}, where => { is => 'ARRAY', is_optional => 1}, calculate => { is => 'Text' , is_optional => 1}, calculate_from => { is => 'ARRAY' , is_optional => 1}, calculate_perl => { is => 'Perl' , is_optional => 1}, calculate_sql => { is => 'SQL' , is_optional => 1}, calculate_js => { is => 'JavaScript' , is_optional => 1}, constraint_name => { is => 'Text' , is_optional => 1}, is_legacy_eav => { is => 'Boolean' , is_optional => 1}, is_dimension => { is => 'Boolean', is_optional => 1}, is_specified_in_module_header => { is => 'Boolean', default_value => 0 }, position_in_module_header => { is => 'Integer', is_optional => 1, doc => "Line in the class definition source's section this property appears" }, singular_name => { is => 'Text' }, plural_name => { is => 'Text' }, class_meta => { is => 'UR::Object::Type', id_by => 'class_name' }, r_class_meta => { is => 'UR::Object::Type', id_by => 'data_type' }, ], unique_constraints => [ { properties => [qw/property_name class_name/], sql => 'SUPER_FAKE_O4' }, ], ); UR::Object::Type->define( class_name => 'UR::Object::Property::Calculated::From', id_properties => [qw/class_name calculated_property_name source_property_name/], ); require UR::Singleton; require UR::Namespace; UR::Object::Type->define( class_name => 'UR', extends => ['UR::Namespace'], ); require UR::Context; UR::Object::Type->initialize_bootstrap_classes; require Command; $UR::initialized = 1; require UR::Change; require UR::Context::Root; require UR::Context::Process; require UR::Object::Tag; do { UR::Context->_initialize_for_current_process(); }; require UR::ModuleLoader; # signs us up with Class::Autouse require UR::Value::Iterator; require UR::Object::View; require UR::Object::Join; sub main::ur_core { print STDERR "Dumping rules and templates to ./ur_core.stor...\n"; my $dump; unless(open($dump, ">ur_core.stor")) { print STDERR "Can't open ur_core.stor for writing: $!"; exit; } store_fd([ $UR::Object::rule_templates, $UR::Object::rules, ], $dump); close $dump; exit(); } 1; __END__ =pod =head1 NAME UR - rich declarative transactional objects =head1 VERSION This document describes UR version 0.44 =head1 SYNOPSIS use UR; ## no database class Foo { is => 'Bar', has => [qw/prop1 prop2 prop3/] }; $o1 = Foo->create(prop1 => 111, prop2 => 222, prop3 => 333); @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object $o1->delete; @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns zero objects @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object again ## database class Animal { has => [ favorite_food => { is => 'Text', doc => "what's yummy?" }, ], data_source => 'MyDB1', table_name => 'Animal' }; class Cat { is => 'Animal', has => [ feet => { is => 'Number', default_value => 4 }, fur => { is => 'Text', valid_values => [qw/fluffy scruffy/] }, ], data_source => 'MyDB1', table_name => 'Cat' }; Cat->create(feet => 4, fur => 'fluffy', favorite_food => 'taters'); @cats = Cat->get(favorite_food => ['taters','sea bass']); $c = $cats[0]; print $c->feet,"\n"; $c->fur('scruffy'); UR::Context->commit(); =head1 DESCRIPTION UR is a class framework and object/relational mapper for Perl. It starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and extends its capabilities with ORM (object-relational mapping) capabilities, object cache, in-memory transactions, more formal class definitions, metadata, documentation system, iterators, command line tools, etc. UR can handle multiple column primary and foreign keys, SQL joins involving class inheritance and relationships, and does its best to avoid querying the database unless the requested data has not been loaded before. It has support for SQLite, Oracle, Mysql and Postgres databases, and the ability to use a text file as a table. UR uses the same syntax to define non-persistent objects, and supports in-memory transactions for both. =head1 DOCUMENTATION =head2 Manuals L - command line interface L - UR from Ten Thousand Feet L - Getting started with UR L - Slides for a presentation on UR L - Recepies for getting stuff working L - UR's metadata system L - Defining classes =head2 Basic Entities L - Pretty much everything is-a UR::Object L - Metadata class for Classes L - Metadata class for Properties L - Manage packages and classes L - Software transactions and More! L - How and where to get data =head1 QUICK TUTORIAL First create a Namespace class for your application, Music.pm: package Music; use UR; class Music { is => 'UR::Namespace' }; 1; Next, define a data source representing your database, Music/DataSource/DB1.pm package Music::DataSource::DB1; use Music; class Music::DataSource::DB1 { is => ['UR::DataSource::MySQL', 'UR::Singleton'], has_constant => [ server => { value => 'database=music' }, owner => { value => 'music' }, login => { value => 'mysqluser' }, auth => { value => 'mysqlpasswd' }, ] }; or to get something going quickly, SQLite has smart defaults... class Music::DataSource::DB1 { is => ['UR::DataSource::SQLite', 'UR::Singleton'], }; Create a class to represent artists, who have many CDs, in Music/Artist.pm package Music::Artist; use Music; class Music::Artist { id_by => 'artist_id', has => [ name => { is => 'Text' }, cds => { is => 'Music::Cd', is_many => 1, reverse_as => 'artist' } ], data_source => 'Music::DataSource::DB1', table_name => 'ARTIST', }; Create a class to represent CDs, in Music/Cd.pm package Music::Cd; use Music; class Music::Cd { id_by => 'cd_id', has => [ artist => { is => 'Music::Artist', id_by => 'artist_id' }, title => { is => 'Text' }, year => { is => 'Integer' }, artist_name => { via => 'artist', to => 'name' }, ], data_source => 'Music::DataSource::DB1', table_name => 'CD', }; If the database does not exist, you can run this to generate the tables and columns from the classes you've written (very experimental): $ cd Music $ ur update schema If the database existed already, you could have done this to get it to write the last 2 classes for you: $ cd Music; $ ur update classes Regardless, if the classes and database tables are present, you can then use these classes in your application code: # Using the namespace enables auto-loading of modules upon first attempt to call a method use Music; # This would get back all Artist objects: my @all_artists = Music::Artist->get(); # After the above, further requests would be cached # if that set were large though, you might want to iterate gradually: my $artist_iter = Music::Artist->create_iterator(); # Get the first object off of the iterator my $first_artist = $artist_iter->next(); # Get all the CDs published in 2007 for the first artist my @cds_2007 = Music::Cd->get(year => 2007, artist => $first_artist); # Use non-equality operators: my @some_cds = Music::Cd->get( 'year between' => ['2004','2009'] ); # This will use a JOIN with the ARTISTS table internally to filter # the data in the database. @some_cds will contain Music::Cd objects. # As a side effect, related Artist objects will be loaded into the cache @some_cds = Music::Cd->get( year => '2007', 'artist_name like' => 'Bob%' ); # These values would be cached... my @artists_for_some_cds = map { $_->artist } @some_cds; # This will use a join to prefetch Artist objects related to the # objects that match the filter my @other_cds = Music::Cd->get( 'title like' => '%White%', -hints => ['artist'] ); my $other_artist_0 = $other_cds[0]->artist; # already loaded so no query # create() instantiates a new object in the current "context", but does not save # it in the database. It will autogenerate its own cd_id: my $new_cd = Music::Cd->create( title => 'Cool Album', year => 2009 ); # Assign it to an artist; fills in the artist_id field of $new_cd $first_artist->add_cd($new_cd); # Save all changes in the current transaction back to the database(s) # which are behind the changed objects. UR::Context->current->commit; =head1 Environment Variables UR uses several environment variables to do things like run with database commits disabled, watching SQL queries run, examine query plans, and control cache size, etc. These make development and debugging fast and easy. See L for details. =head1 DEPENDENCIES Class::Autouse Cwd Data::Dumper Date::Format DBI File::Basename FindBin FreezeThaw Path::Class Scalar::Util Sub::Installer Sub::Name Sys::Hostname Text::Diff Time::HiRes XML::Simple =head1 AUTHORS UR was built by the software development team at the McDonnell Genome Institute at Washington University School of Medicine (Richard K. Wilson, PI). Incarnations of it run laboratory automation and analysis systems for high-throughput genomics. Anthony Brummett brummett@cpan.org Nathan Nutter Josh McMichael Eric Clark Ben Oberkfell Eddie Belter Feiyu Du Adam Dukes Brian Derickson Craig Pohl Gabe Sanderson Todd Hepler Jason Walker James Weible Indraniel Das Shin Leong Ken Swanson Scott Abbott Alice Diec William Schroeder Shawn Leonard Lynn Carmichael Amy Hawkins Michael Kiwala Kevin Crouse Mark Johnson Kyung Kim Jon Schindler Justin Lolofie Jerome Peirick Ryan Richt John Osborne Chris Harris Philip Kimmey Robert Long Travis Abbott Matthew Callaway James Eldred Scott Smith sakoht@cpan.org David Dooling =head1 LICENCE AND COPYRIGHT Copyright (C) 2002-2015 Washington University in St. Louis, MO. This sofware is licensed under the same terms as Perl itself. See the LICENSE file in this distribution. =pod All.pm100664023532023421 2122412544604516 14020 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::All; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; BEGIN { require above; }; use UR; use Command; use Command::DynamicSubCommands; use Command::Test; use Command::Test::Echo; use Command::Test::Tree1; use Command::Test::Tree1::Echo1; use Command::Test::Tree1::Echo2; use Command::Tree; use Command::V1; use Command::V2; use Devel::callcount; use UR::BoolExpr; use UR::BoolExpr::Template; use UR::BoolExpr::Template::And; use UR::BoolExpr::Template::Composite; use UR::BoolExpr::Template::Or; use UR::BoolExpr::Template::PropertyComparison; use UR::BoolExpr::Template::PropertyComparison::Between; use UR::BoolExpr::Template::PropertyComparison::Equals; use UR::BoolExpr::Template::PropertyComparison::False; use UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual; use UR::BoolExpr::Template::PropertyComparison::GreaterThan; use UR::BoolExpr::Template::PropertyComparison::In; use UR::BoolExpr::Template::PropertyComparison::LessOrEqual; use UR::BoolExpr::Template::PropertyComparison::LessThan; use UR::BoolExpr::Template::PropertyComparison::Like; use UR::BoolExpr::Template::PropertyComparison::Matches; use UR::BoolExpr::Template::PropertyComparison::NotEquals; use UR::BoolExpr::Template::PropertyComparison::NotIn; use UR::BoolExpr::Template::PropertyComparison::NotLike; use UR::BoolExpr::Template::PropertyComparison::True; use UR::BoolExpr::Util; use UR::Change; use UR::Context; use UR::Context::DefaultRoot; use UR::Context::ObjectFabricator; use UR::Context::Process; use UR::Context::Root; use UR::Context::Transaction; use UR::DataSource; use UR::DataSource::Code; use UR::DataSource::CSV; use UR::DataSource::Default; use UR::DataSource::File; use UR::DataSource::FileMux; use UR::DataSource::Meta; BEGIN { eval { require DBD::mysql }; require UR::DataSource::MySQL unless $@; } BEGIN { eval { require DBD::Oracle }; require UR::DataSource::Oracle unless $@; } BEGIN { eval { require DBD::Pg }; require UR::DataSource::Pg unless $@; } use UR::DataSource::RDBMS; use UR::DataSource::RDBMS::BitmapIndex; use UR::DataSource::RDBMS::Entity; use UR::DataSource::RDBMS::FkConstraint; use UR::DataSource::RDBMS::FkConstraintColumn; use UR::DataSource::RDBMS::PkConstraintColumn; use UR::DataSource::RDBMS::Table; use UR::DataSource::RDBMS::Table::View::Default::Text; use UR::DataSource::RDBMS::TableColumn; use UR::DataSource::RDBMS::TableColumn::View::Default::Text; use UR::DataSource::RDBMS::UniqueConstraintColumn; use UR::DataSource::SQLite; use UR::DataSource::ValueDomain; use UR::DBI; use UR::Debug; use UR::DeletedRef; use UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES; use UR::Env::UR_CONTEXT_BASE; use UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER; use UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER; use UR::Env::UR_CONTEXT_MONITOR_QUERY; use UR::Env::UR_CONTEXT_ROOT; use UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT; use UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK; use UR::Env::UR_DBI_EXPLAIN_SQL_IF; use UR::Env::UR_DBI_EXPLAIN_SQL_MATCH; use UR::Env::UR_DBI_EXPLAIN_SQL_SLOW; use UR::Env::UR_DBI_MONITOR_DML; use UR::Env::UR_DBI_MONITOR_EVERY_FETCH; use UR::Env::UR_DBI_MONITOR_SQL; use UR::Env::UR_DBI_NO_COMMIT; use UR::Env::UR_DEBUG_OBJECT_PRUNING; use UR::Env::UR_DEBUG_OBJECT_RELEASE; use UR::Env::UR_IGNORE; use UR::Env::UR_NR_CPU; use UR::Env::UR_STACK_DUMP_ON_DIE; use UR::Env::UR_STACK_DUMP_ON_WARN; use UR::Env::UR_TEST_QUIET; use UR::Env::UR_USE_ANY; use UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS; use UR::Env::UR_USED_LIBS; use UR::Env::UR_USED_MODS; use UR::Exit; use UR::ModuleBase; use UR::ModuleBuild; use UR::ModuleConfig; use UR::ModuleLoader; use UR::Namespace; use UR::Namespace::Command; use UR::Namespace::Command::Base; use UR::Namespace::Command::Define; use UR::Namespace::Command::Define::Class; use UR::Namespace::Command::Define::Datasource; use UR::Namespace::Command::Define::Datasource::File; use UR::Namespace::Command::Define::Datasource::Mysql; use UR::Namespace::Command::Define::Datasource::Oracle; use UR::Namespace::Command::Define::Datasource::Pg; use UR::Namespace::Command::Define::Datasource::Rdbms; use UR::Namespace::Command::Define::Datasource::RdbmsWithAuth; use UR::Namespace::Command::Define::Datasource::Sqlite; use UR::Namespace::Command::Define::Db; use UR::Namespace::Command::Define::Namespace; use UR::Namespace::Command::Show::Properties; use UR::Namespace::Command::Show::Schema; use UR::Namespace::Command::Init; use UR::Namespace::Command::List; use UR::Namespace::Command::List::Classes; use UR::Namespace::Command::List::Modules; use UR::Namespace::Command::List::Objects; use UR::Namespace::Command::Old; use UR::Namespace::Command::Old::DiffRewrite; use UR::Namespace::Command::Old::DiffUpdate; use UR::Namespace::Command::Old::ExportDbicClasses; use UR::Namespace::Command::Old::Info; use UR::Namespace::Command::Old::Redescribe; use UR::Namespace::Command::RunsOnModulesInTree; use UR::Namespace::Command::Sys; use UR::Namespace::Command::Sys::ClassBrowser; use UR::Namespace::Command::Test; use UR::Namespace::Command::Test::Callcount; use UR::Namespace::Command::Test::Callcount::List; use UR::Namespace::Command::Test::Compile; use UR::Namespace::Command::Test::Eval; use UR::Namespace::Command::Test::Run; use UR::Namespace::Command::Test::TrackObjectRelease; use UR::Namespace::Command::Test::Use; use UR::Namespace::Command::Test::Window; use UR::Namespace::Command::Update; use UR::Namespace::Command::Update::ClassDiagram; use UR::Namespace::Command::Update::ClassesFromDb; use UR::Namespace::Command::Update::Pod; use UR::Namespace::Command::Update::RenameClass; use UR::Namespace::Command::Update::RewriteClassHeader; use UR::Namespace::Command::Update::SchemaDiagram; use UR::Namespace::Command::Update::TabCompletionSpec; use UR::Object; use UR::Object::Accessorized; use UR::Object::Command::FetchAndDo; use UR::Object::Command::List; use UR::Object::Command::List::Style; use UR::Object::Ghost; use UR::Object::Index; use UR::Object::Iterator; use UR::Object::Property; use UR::Object::Property::View::Default::Text; use UR::Object::Property::View::DescriptionLineItem::Text; use UR::Object::Property::View::ReferenceDescription::Text; use UR::Object::Set; use UR::Object::Set::View::Default::Json; use UR::Object::Tag; use UR::Object::Type; use UR::Object::Type::AccessorWriter; use UR::Object::Type::AccessorWriter::Product; use UR::Object::Type::AccessorWriter::Sum; use UR::Object::Type::Initializer; use UR::Object::Type::InternalAPI; use UR::Object::Type::ModuleWriter; use UR::Object::Type::View::Default::Text; use UR::Object::Value; use UR::Object::View; use UR::Object::View::Aspect; use UR::Object::View::Default::Gtk; use UR::Object::View::Default::Gtk2; use UR::Object::View::Default::Json; use UR::Object::View::Default::Text; use UR::Object::View::Lister::Text; use UR::Object::View::Toolkit; use UR::Object::View::Toolkit::Text; use UR::ObjectDeprecated; use UR::ObjectV001removed; use UR::ObjectV04removed; use UR::Observer; use UR::DBI::Report; use UR::Service::RPC::Executer; use UR::Service::RPC::Message; use UR::Service::RPC::Server; use UR::Service::RPC::TcpConnectionListener; use UR::Singleton; use UR::Util; use UR::Value; use UR::Value::ARRAY; use UR::Value::Blob; use UR::Value::CSV; use UR::Value::DateTime; use UR::Value::Decimal; use UR::Value::DirectoryPath; use UR::Value::FilePath; use UR::Value::FilesystemPath; use UR::Value::FOF; use UR::Value::HASH; use UR::Value::Integer; use UR::Value::Iterator; use UR::Value::Number; use UR::Value::PerlReference; use UR::Value::SCALAR; use UR::Value::Set; use UR::Value::Text; use UR::Value::URL; use UR::Vocabulary; # optional elements if (eval "use Net::HTTPServer") { my $rv = eval "UR::Namespace::View::SchemaBrowser::CgiApp;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Base;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Class;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::File;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Index;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Schema;" && eval "use UR::Service::JsonRpcServer;"; die $@ unless ($rv); } if (eval "use Xml::LibXSLT") { my $rv = eval "use UR::Object::View::Default::Html;" && eval "use UR::Object::View::Default::Xsl;" && eval "use UR::Object::Set::View::Default::Xml;" && eval "use UR::Object::View::Default::Xml;" && eval "use UR::Object::Type::View::Default::Xml;" ; die $@ unless ($rv); } 1; __END__ =pod =head1 NAME UR::All =head1 SYNOPSIS use UR::All; =head1 DESCRIPTION This module exists to let software preload everything in the distribution It is slower than "use UR", but is good for things like FastCGI servers. =cut BoolExpr.pm100664023532023421 14072512544604516 15072 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::BoolExpr; use warnings; use strict; use List::MoreUtils qw(uniq); use List::Util qw(first); use Scalar::Util qw(blessed); require UR; use Carp; our @CARP_NOT = ('UR::Context'); our $VERSION = "0.44"; # UR $VERSION;; # readable stringification use overload ('""' => '__display_name__'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); UR::Object::Type->define( class_name => 'UR::BoolExpr', composite_id_separator => $UR::BoolExpr::Util::id_sep, id_by => [ template_id => { type => 'Blob' }, value_id => { type => 'Blob' }, ], has => [ template => { is => 'UR::BoolExpr::Template', id_by => 'template_id' }, subject_class_name => { via => 'template' }, logic_type => { via => 'template' }, logic_detail => { via => 'template' }, num_values => { via => 'template' }, is_normalized => { via => 'template' }, is_id_only => { via => 'template' }, has_meta_options => { via => 'template' }, ], is_transactional => 0, ); # for performance sub UR::BoolExpr::Type::resolve_composite_id_from_ordered_values { shift; return join($UR::BoolExpr::Util::id_sep,@_); } # only respect the first delimiter instead of splitting sub UR::BoolExpr::Type::resolve_ordered_values_from_composite_id { my ($self,$id) = @_; my $pos = index($id,$UR::BoolExpr::Util::id_sep); return (substr($id,0,$pos), substr($id,$pos+1)); } sub template { my $self = $_[0]; return $self->{template} ||= $self->__template; } sub flatten { my $self = shift; return $self->{flatten} if exists $self->{flatten}; my $flat = $self->template->_flatten_bx($self); $self->{flatten} = $flat; Scalar::Util::weaken($self->{flatten}) if $self == $flat; return $flat; } sub reframe { my $self = shift; my $in_terms_of = shift; return $self->{reframe}{$in_terms_of} if $self->{reframe}{$in_terms_of}; my $reframe = $self->template->_reframe_bx($self, $in_terms_of); $self->{reframe}{$in_terms_of} = $reframe; Scalar::Util::weaken($self->{reframe}{$in_terms_of}) if $self == $reframe; return $reframe; } # override the UR/system display name # this is used in stringification overload sub __display_name__ { my $self = shift; my %b = $self->_params_list; my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Sortkeys(1)->Dump; $s =~ s/\n/ /gs; $s =~ s/^\s*{//; $s =~ s/\}\s*$//; $s =~ s/\"(\w+)\" \=\> / $1 => /g; return __PACKAGE__ . '=(' . $self->subject_class_name . ':' . $s . ')'; } # The primary function: evaluate a subject object as matching the rule or not. sub evaluate { my $self = shift; my $subject = shift; my $template = $self->template; my @values = $self->values; return $template->evaluate_subject_and_values($subject,@values); } # Behind the id properties: sub template_and_values { my $self = shift; my ($template_id, $value_id) = UR::BoolExpr::Type->resolve_ordered_values_from_composite_id($self->id); return (UR::BoolExpr::Template->get($template_id), UR::BoolExpr::Util::value_id_to_values($value_id)); } # Returns true if the rule represents a subset of the things the other # rule would match. It returns undef if the answer is not known, such as # when one of the values is a list and we didn't go to the trouble of # searching the list for a matching value sub is_subset_of { my($self, $other_rule) = @_; return 0 unless (ref($other_rule) and $self->isa(ref $other_rule)); my $my_template = $self->template; my $other_template = $other_rule->template; unless ($my_template->isa("UR::BoolExpr::Template::And") and $other_template->isa("UR::BoolExpr::Template::And")) { Carp::confess("This method currently works only on ::And expressions. Update to handle ::Or, ::PropertyComparison, and templates of mismatched class!"); } return unless ($my_template->is_subset_of($other_template)); my $values_match = 1; foreach my $prop ( $other_template->_property_names ) { my $my_operator = $my_template->operator_for($prop) || '='; my $other_operator = $other_template->operator_for($prop) || '='; my $my_value = $self->value_for($prop); my $other_value = $other_rule->value_for($prop); # If either is a list of values, return undef return undef if (ref($my_value) || ref($other_value)); no warnings 'uninitialized'; $values_match = undef if ($my_value ne $other_value); } return $values_match; } sub values { my $self = shift; if ($self->{values}) { return @{ $self->{values}} } my $value_id = $self->value_id; return unless defined($value_id) and length($value_id); my @values; @values = UR::BoolExpr::Util::value_id_to_values($value_id); if (my $hard_refs = $self->{hard_refs}) { for my $n (keys %$hard_refs) { $values[$n] = $hard_refs->{$n}; } } $self->{values} = \@values; return @values; } sub value_for_id { my $self = shift; my $t = $self->template; my $position = $t->id_position; return unless defined $position; return $self->value_for_position($position); } sub specifies_value_for { my $self = shift; my $rule_template = $self->template; return $rule_template->specifies_value_for(@_); } sub value_for { my $self = shift; my $property_name = shift; # TODO: refactor to be more efficient my $template = $self->template; my $h = $self->legacy_params_hash; my $v; if (exists $h->{$property_name}) { # normal case $v = $h->{$property_name}; my $tmpl_pos = $template->value_position_for_property_name($property_name); if (exists $self->{'hard_refs'}->{$tmpl_pos}) { $v = $self->{'hard_refs'}->{$tmpl_pos}; # It was stored during resolve() as a hard ref } elsif ($self->_value_is_old_style_operator_and_value($v)) { $v = $v->{'value'}; # It was old style operator/value hash } } else { # No value found under that name... try decomposing the id return if $property_name eq 'id'; my $id_value = $self->value_for('id'); my $class_meta = $self->subject_class_name->__meta__(); my @id_property_values = $class_meta->get_composite_id_decomposer->($id_value); my @id_property_names = $class_meta->id_property_names; for (my $i = 0; $i < @id_property_names; $i++) { if ($id_property_names[$i] eq $property_name) { $v = $id_property_values[$i]; last; } } } return $v; } sub value_for_position { my ($self, $pos) = @_; return ($self->values)[$pos]; } sub operator_for { my $self = shift; my $t = $self->template; return $t->operator_for(@_); } sub underlying_rules { my $self = shift; unless (exists $self->{'_underlying_rules'}) { my @values = $self->values; $self->{'_underlying_rules'} = [ $self->template->get_underlying_rules_for_values(@values) ]; } return @{ $self->{'_underlying_rules'} }; } # De-compose the rule back into its original form. sub params_list { # This is the reverse of the bulk of resolve. # It returns the params in list form, directly coercable into a hash if necessary. # $r = UR::BoolExpr->resolve($c1,@p1); # ($c2, @p2) = ($r->subject_class_name, $r->params_list); my $self = shift; my $template = $self->template; my @values_sorted = $self->values; return $template->params_list_for_values(@values_sorted); } # TODO: replace these with the logical set operations # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object sub add_filter { my $self = shift; return __PACKAGE__->resolve($self->subject_class_name, $self->params_list, @_); } # TODO: replace these with the logical set operations # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object sub remove_filter { my $self = shift; my $property_name = shift; my @params_list = $self->params_list; my @new_params_list; for (my $n=0; $n<=$#params_list; $n+=2) { my $key = $params_list[$n]; if ($key =~ /^$property_name\b/) { next; } my $value = $params_list[$n+1]; push @new_params_list, $key, $value; } return __PACKAGE__->resolve($self->subject_class_name, @new_params_list); } # as above, doesn't mutate, just returns a different bx sub sub_classify { my ($self,$subclass_name) = @_; my ($t,@v) = $self->template_and_values(); return $t->sub_classify($subclass_name)->get_rule_for_values(@v); } # flyweight constructor # like regular UR::Value objects, but kept separate from the cache but kept # out of the regular transaction cache so they alwasy vaporize when derefed sub get { my $rule_id = pop; unless (exists $UR::Object::rules->{$rule_id}) { my $pos = index($rule_id,$UR::BoolExpr::Util::id_sep); my ($template_id,$value_id) = (substr($rule_id,0,$pos), substr($rule_id,$pos+1)); my $rule = { id => $rule_id, template_id => $template_id, value_id => $value_id }; bless ($rule, "UR::BoolExpr"); $UR::Object::rules->{$rule_id} = $rule; Scalar::Util::weaken($UR::Object::rules->{$rule_id}); return $rule; } return $UR::Object::rules->{$rule_id}; } # because these are weakened sub DESTROY { delete $UR::Object::rules->{$_[0]->{id}}; } sub flatten_hard_refs { my $self = $_[0]; return $self if not $self->{hard_refs}; my $subject_class_name = $self->subject_class_name; my $meta = $subject_class_name->__meta__; my %params = $self->_params_list; my $changes = 0; for my $key (keys %params) { my $value = $params{$key}; if (ref($value) and Scalar::Util::blessed($value) and $value->isa("UR::Object")) { my ($property_name, $op) = ($key =~ /^(\S+)\s*(.*)/); my $pmeta = $meta->property($property_name); my $final_pmeta = $pmeta->final_property_meta(); my @possible_data_types = uniq grep { $_ } map { $_->data_type } ($pmeta, $final_pmeta); unless (@possible_data_types) { # this might not be possible at runtime croak sprintf 'unable to determine data type for property: %s', $property_name; } my $data_type = first { $value->isa($_) } @possible_data_types; unless ($data_type) { croak sprintf 'value type, %s, is incompatible with: %s', $value->class, join(', ', @possible_data_types); } my $value2 = eval { $data_type->get($value->id) }; unless ($value2) { croak sprintf 'unable to retrieve a %s by value ID: %s', $data_type, $value->id; } unless ($value2 eq $value) { croak sprintf 'retrieved duplicate %s with ID, %s,', $data_type, $value->id; } # safe to re-represent as .id my $new_key = $property_name . '.id'; $new_key .= ' ' . $op if $op; delete $params{$key}; $params{$new_key} = $value->id; $changes++; } } if ($changes) { return $self->resolve_normalized($subject_class_name, %params); } else { return $self; } } sub resolve_normalized { my $class = shift; my ($unnormalized_rule, @extra) = $class->resolve(@_); my $normalized_rule = $unnormalized_rule->normalize(); return if !defined(wantarray); return ($normalized_rule,@extra) if wantarray; if (@extra) { no warnings; my $rule_class = $normalized_rule->subject_class_name; Carp::confess("Extra params for class $rule_class found: @extra\n"); } return $normalized_rule; } sub resolve_for_template_id_and_values { my ($class,$template_id, @values) = @_; my $value_id = UR::BoolExpr::Util::values_to_value_id(@values); my $rule_id = $class->__meta__->resolve_composite_id_from_ordered_values($template_id,$value_id); $class->get($rule_id); } # Return true if it's a hashref that specifies the old-style operator/value # like property => { operator => '=', value => 1 } # FYI, the new way to do this is: # 'property =' => 1 sub _value_is_old_style_operator_and_value { my($class,$value) = @_; return (ref($value) eq 'HASH') && (exists($value->{'operator'})) && (exists($value->{'value'})) && ( (keys(%$value) == 2) || ((keys(%$value) == 3) && exists($value->{'escape'})) ); } my $resolve_depth; sub resolve { $resolve_depth++; Carp::confess("Deep recursion in UR::BoolExpr::resolve()!") if $resolve_depth > 10; # handle the case in which we've already processed the params into a boolexpr if ( @_ == 3 and ref($_[2]) and ref($_[2])->isa("UR::BoolExpr") ) { $resolve_depth--; return $_[2]; } my $class = shift; my $subject_class = shift; Carp::confess("Can't resolve BoolExpr: expected subject class as arg 2, got '$subject_class'") if not $subject_class; # support for legacy passing of hashref instead of object or list # TODO: eliminate the need for this my @in_params; if ($subject_class->isa('UR::Value::PerlReference') and $subject_class eq 'UR::Value::' . ref($_[0])) { @in_params = @_; } elsif (ref($_[0]) eq "HASH") { @in_params = %{$_[0]}; } else { @in_params = @_; } if (defined($in_params[0]) and $in_params[0] eq '-or') { shift @in_params; my @sub_queries = @{ shift @in_params }; my @meta_params; for (my $i = 0; $i < @in_params; $i += 2 ) { if ($in_params[$i] =~ m/^-/) { push @meta_params, $in_params[$i], $in_params[$i+1]; } } my $bx = UR::BoolExpr::Template::Or->_compose( $subject_class, \@sub_queries, \@meta_params, ); $resolve_depth--; return $bx; } if (@in_params == 1) { unshift @in_params, "id"; } elsif (@in_params % 2 == 1) { Carp::carp("Odd number of params while creating $class: (",join(',',@in_params),")"); } # split the params into keys and values # where an operator is on the right-side, it is moved into the key my $count = @in_params; my (@keys,@values,@constant_values,$key,$value,$property_name,$operator,@hard_refs); for(my $n = 0; $n < $count;) { $key = $in_params[$n++]; $value = $in_params[$n++]; unless (defined $key) { Carp::croak("Can't resolve BoolExpr: undef is an invalid key/property name. Args were: ".join(', ',@in_params)); } if (UR::BoolExpr::Util::is_meta_param($key)) { # these are keys whose values live in the rule template push @keys, $key; push @constant_values, $value; next; } if ($key =~ m/^(_id_only|_param_key|_unique|__get_serial|_change_count)$/) { # skip the pair: legacy/internal cruft next; } my $pos = index($key,' '); if ($pos != -1) { # the key is "propname op" $property_name = substr($key,0,$pos); $operator = substr($key,$pos+1); if (substr($operator,0,1) eq ' ') { $operator =~ s/^\s+//; } } else { # the key is "propname" $property_name = $key; $operator = ''; } if (my $ref = ref($value)) { if ( (not $operator) and ($ref eq "HASH")) { if ( $class->_value_is_old_style_operator_and_value($value)) { # the key => { operator => $o, value => $v } syntax # cannot be used with a value type of HASH $operator = defined($value->{operator}) ? lc($value->{operator}) : ''; if (exists $value->{escape}) { $operator .= "-" . $value->{escape} } $key .= " " . $operator; $value = $value->{value}; $ref = ref($value); } else { # the HASH is a value for the specified param push @hard_refs, scalar(@values), $value; } } if ($ref eq "ARRAY") { if (not $operator) { # key => [] is the same as "key in" => [] $operator = 'in'; $key .= ' in'; } elsif ($operator eq 'not') { # "key not" => [] is the same as "key not in" $operator .= ' in'; $key .= ' in'; } foreach my $val (@$value) { if (ref($val)) { # when there are any refs in the arrayref # we must keep the arrayerf contents # to reconstruct effectively push @hard_refs, scalar(@values), $value; last; } } } # done handling ARRAY value } # done handling ref values push @keys, $key; push @values, $value; } # the above uses no class metadata # this next section uses class metadata # it should be moved into the normalization layer my $subject_class_meta = eval { $subject_class->__meta__ }; if ($@) { Carp::croak("Can't get class metadata for $subject_class. Is it a valid class name?\nErrors were: $@"); } unless ($subject_class_meta) { Carp::croak("No class metadata for $subject_class?!"); } my $subject_class_props = $subject_class_meta->{'cache'}{'UR::BoolExpr::resolve'} ||= { map {$_, 1} ( $subject_class_meta->all_property_type_names) }; my($kn, $vn, $cn, $complex_values) = (0,0,0,0); my ($op,@extra,@xadd_keys,@xadd_values,@xremove_keys,@xremove_values,@extra_key_pos,@extra_value_pos, @swap_key_pos,@swap_key_value); for my $value (@values) { $key = $keys[$kn++]; if (UR::BoolExpr::Util::is_meta_param($key)) { $cn++; redo; } else { $vn++; } my $pos = index($key,' '); if ($pos != -1) { # "propname op" $property_name = substr($key,0,$pos); $operator = substr($key,$pos+1); if (substr($operator,0,1) eq ' ') { $operator =~ s/^\s+//; } } else { # "propname" $property_name = $key; $operator = ''; } # account for the case where this parameter does # not match an actual property my $base_property_name = $property_name; $base_property_name =~ s/[.-].+//; if (!exists $subject_class_props->{$base_property_name}) { if (substr($property_name,0,1) eq '_') { warn "ignoring $property_name in $subject_class bx construction!" } else { push @extra_key_pos, $kn-1; push @extra_value_pos, $vn-1; next; } } my $ref = ref($value); if($ref) { $complex_values = 1; if ($ref eq "ARRAY" and $operator ne 'between' and $operator ne 'not between') { my $data_type; my $is_many; if ($UR::initialized) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless (defined $property_meta) { push @extra_key_pos, $kn-1; push @extra_value_pos, $vn-1; next; } $data_type = $property_meta->data_type; $is_many = $property_meta->is_many; } else { if (exists $subject_class_meta->{has}{$property_name}) { $data_type = $subject_class_meta->{has}{$property_name}{data_type}; $is_many = $subject_class_meta->{has}{$property_name}{is_many}; } } $data_type ||= ''; if ($data_type eq 'ARRAY') { # ensure we re-constitute the original array not a copy push @hard_refs, $vn-1, $value; push @swap_key_pos, $vn-1; push @swap_key_value, $property_name; } elsif (not $is_many) { no warnings; # sort and replace # note that in perl5.10 and above strings like "inf*" have a numeric value # causing this kind of sorting to do surprising things. Hopefully looks_like_number() # does the right thing with these. # # undef/null sorts at the end my $sorter = sub { if (! defined($a)) { return 1 } if (! defined($b)) { return -1} return $a cmp $b; }; $value = [ sort $sorter @$value ]; # Remove duplicates from the list my $last = $value; for (my $i = 0; $i < @$value;) { if ($last eq $value->[$i]) { splice(@$value, $i, 1); } else { $last = $value->[$i++]; } } # push @swap_key_pos, $vn-1; # push @swap_key_value, $property_name; } else { # disable: break 47, enable: break 62 #push @swap_key_pos, $vn-1; #push @swap_key_value, $property_name; } } elsif (blessed($value)) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless ($property_meta) { for my $class_name ($subject_class_meta->ancestry_class_names) { my $class_object = $class_name->__meta__; $property_meta = $subject_class_meta->property_meta_for_name($property_name); last if $property_meta; } unless ($property_meta) { Carp::croak("No property metadata for $subject_class property '$property_name'"); } } if ($property_meta->id_by or $property_meta->reverse_as) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless ($property_meta) { Carp::croak("No property metadata for $subject_class property '$property_name'"); } my @joins = $property_meta->get_property_name_pairs_for_join(); for my $join (@joins) { # does this really work for >1 joins? my ($my_method, $their_method) = @$join; push @xadd_keys, $my_method; push @xadd_values, $value->$their_method; } # TODO: this may need to be moved into the above get_property_name_pairs_for_join(), # but the exact syntax for expressing that this is part of the join is unclear. if (my $id_class_by = $property_meta->id_class_by) { push @xadd_keys, $id_class_by; push @xadd_values, ref($value); } push @xremove_keys, $kn-1; push @xremove_values, $vn-1; } elsif ($property_meta->is_valid_storage_for_value($value)) { push @hard_refs, $vn-1, $value; } elsif ($value->can($property_name)) { # TODO: stop suporting foo_id => $foo, since you can do foo=>$foo, and foo_id=>$foo->id # Carp::cluck("using $property_name => \$obj to get $property_name => \$obj->$property_name is deprecated..."); $value = $value->$property_name; } else { $operator = 'eq' unless $operator; $DB::single = 1; print $value->isa($property_meta->_data_type_as_class_name),"\n"; print $value->isa($property_meta->_data_type_as_class_name),"\n"; Carp::croak("Invalid data type in rule. A value of type " . ref($value) . " cannot be used in class $subject_class property '$property_name' with operator $operator!"); } # end of handling a value which is an arrayref } elsif ($ref ne 'HASH') { # other reference, code, etc. push @hard_refs, $vn-1, $value; } } } push @keys, @xadd_keys; push @values, @xadd_values; if (@swap_key_pos) { @keys[@swap_key_pos] = @swap_key_value; } if (@extra_key_pos) { push @xremove_keys, @extra_key_pos; push @xremove_values, @extra_value_pos; for (my $n = 0; $n < @extra_key_pos; $n++) { push @extra, $keys[$extra_key_pos[$n]], $values[$extra_value_pos[$n]]; } } if (@xremove_keys) { my $write_key_idx = 0; for (my($read_key_idx, $xremove_key_idx) = (0,0); $read_key_idx < @keys; $read_key_idx++ ) { if ($xremove_key_idx < @xremove_keys and $read_key_idx == $xremove_keys[$xremove_key_idx] ) { $xremove_key_idx++; next; } $keys[$write_key_idx++] = $keys[$read_key_idx]; } $#keys = $write_key_idx-1; } if (@xremove_values) { if (@hard_refs) { # shift the numbers down to account for positional removals for (my $n = 0; $n < @hard_refs; $n+=2) { my $ref_pos = $hard_refs[$n]; for my $rem_pos (@xremove_values) { if ($rem_pos < $ref_pos) { $hard_refs[$n] -= 1; #print "$n from $ref_pos to $hard_refs[$n]\n"; $ref_pos = $hard_refs[$n]; } elsif ($rem_pos == $ref_pos) { $hard_refs[$n] = ''; $hard_refs[$n+1] = undef; } } } } my $write_value_idx = 0; for(my($read_value_idx, $xremove_value_idx) = (0,0); $read_value_idx < @values; $read_value_idx++ ) { if ($xremove_value_idx < @xremove_values and $read_value_idx == $xremove_values[$xremove_value_idx] ) { $xremove_value_idx++; next; } $values[$write_value_idx++] = $values[$read_value_idx]; } $#values = $write_value_idx-1; } my $template; if (@constant_values) { $template = UR::BoolExpr::Template::And->_fast_construct( $subject_class, \@keys, \@constant_values, ); } else { $template = $subject_class_meta->{cache}{"UR::BoolExpr::resolve"}{"template for class and keys without constant values"}{"$subject_class @keys"} ||= UR::BoolExpr::Template::And->_fast_construct( $subject_class, \@keys, \@constant_values, ); } my $value_id = UR::BoolExpr::Util::values_to_value_id(@values); my $rule_id = join($UR::BoolExpr::Util::id_sep,$template->{id},$value_id); my $rule = __PACKAGE__->get($rule_id); # flyweight constructor $rule->{template} = $template; $rule->{values} = \@values; $vn = 0; $cn = 0; my @list; for my $key (@keys) { push @list, $key; if (UR::BoolExpr::Util::is_meta_param($key)) { push @list, $constant_values[$cn++]; } else { push @list, $values[$vn++]; } } $rule->{_params_list} = \@list; if (@hard_refs) { $rule->{hard_refs} = { @hard_refs }; delete $rule->{hard_refs}{''}; } $resolve_depth--; if (wantarray) { return ($rule, @extra); } elsif (@extra && defined wantarray) { Carp::confess("Unknown parameters in rule for $subject_class: " . join(",", map { defined($_) ? "'$_'" : "(undef)" } @extra)); } else { return $rule; } } sub _params_list { my $list = $_[0]->{_params_list} ||= do { my $self = $_[0]; my $template = $self->template; $self->values unless $self->{values}; my @list; # are method calls really too expensive here? my $template_class = ref($template); if ($template_class eq 'UR::BoolExpr::Template::And') { my ($k,$v,$c) = ($template->{_keys}, $self->{values}, $template->{_constant_values}); my $vn = 0; my $cn = 0; for my $key (@$k) { push @list, $key; if (UR::BoolExpr::Util::is_meta_param($key)) { push @list, $c->[$cn++]; } else { push @list, $v->[$vn++]; } } } elsif ($template_class eq 'UR::BoolExpr::Template::Or') { my @sublist; my @u = $self->underlying_rules(); for my $u (@u) { my @p = $u->_params_list; push @sublist, \@p; } @list = (-or => \@sublist); } elsif ($template_class->isa("UR::BoolExpr::PropertyComparison")) { @list = ($template->logic_detail => [@{$self->{values}}]); } \@list; }; return @$list; } sub normalize { my $self = shift; my $rule_template = $self->template; if ($rule_template->{is_normalized}) { return $self; } my @unnormalized_values = $self->values(); my $normalized = $rule_template->get_normalized_rule_for_values(@unnormalized_values); return unless defined $normalized; if (my $special = $self->{hard_refs}) { $normalized->{hard_refs} = $rule_template->_normalize_non_ur_values_hash($special); } return $normalized; } # a handful of places still use this sub legacy_params_hash { my $self = shift; # See if we have one already. my $params_array = $self->{legacy_params_array}; return { @$params_array } if $params_array; # Make one by starting with the one on the rule template my $rule_template = $self->template; my $params = { %{$rule_template->legacy_params_hash}, $self->params_list }; # If the template has a _param_key, fill it in. if (exists $params->{_param_key}) { $params->{_param_key} = $self->id; } # This was cached above and will return immediately on the next call. # Note: the caller should copy this reference before making changes. $self->{legacy_params_array} = [ %$params ]; return $params; } my $LOADED_BXPARSE = 0; sub resolve_for_string { my ($class, $subject_class_name, $filter_string, $usage_hints_string, $order_string, $page_string) = @_; unless ($LOADED_BXPARSE) { eval { require UR::BoolExpr::BxParser }; if ($@) { Carp::croak("resolve_for_string() can't load UR::BoolExpr::BxParser: $@"); } $LOADED_BXPARSE=1; } #$DB::single=1; #my $tree = UR::BoolExpr::BxParser::parse($filter_string, tokdebug => 1, yydebug => 7); my($tree, $remaining_strref) = UR::BoolExpr::BxParser::parse($filter_string); unless ($tree) { Carp::croak("resolve_for_string() couldn't parse string \"$filter_string\""); } push @$tree, '-hints', [split(',',$usage_hints_string) ] if ($usage_hints_string); push @$tree, '-order_by', [split(',',$order_string) ] if ($order_string); push @$tree, '-page', [split(',',$page_string) ] if ($page_string); my ($bx, @extra); if(wantarray) { ($bx, @extra) = UR::BoolExpr->resolve($subject_class_name, @$tree); } else { $bx = UR::BoolExpr->resolve($subject_class_name, @$tree); } unless ($bx) { Carp::croak("Can't create BoolExpr on $subject_class_name from params generated from string " . $filter_string . " which parsed as:\n" . Data::Dumper::Dumper($tree)); } if ($$remaining_strref) { Carp::croak("Trailing input after the parsable end of the filter string: '". $$remaining_strref."'"); } if(wantarray) { return ($bx, @extra); } else { return $bx; } } sub _resolve_from_filter_array { my $class = shift; my $subject_class_name = shift; my $filters = shift; my $usage_hints = shift; my $order = shift; my $page = shift; my @rule_filters; my @keys; my @values; for my $fdata (@$filters) { my $rule_filter; # rule component my $key = $fdata->[0]; my $value; # process the operator if ($fdata->[1] =~ /^!?(:|@|between|in)$/i) { my @list_parts; my @range_parts; if ($fdata->[1] eq "@") { # file path my $fh = IO::File->new($fdata->[2]); unless ($fh) { die "Failed to open file $fdata->[2]: $!\n"; } @list_parts = $fh->getlines; chomp @list_parts; $fh->close; } else { @list_parts = split(/\//,$fdata->[2]); @range_parts = split(/-/,$fdata->[2]); } if (@list_parts > 1) { my $op = ($fdata->[1] =~ /^!/ ? 'not in' : 'in'); # rule component if (substr($key, -3, 3) ne ' in') { $key = join(' ', $key, $op); } $value = \@list_parts; $rule_filter = [$fdata->[0],$op,\@list_parts]; } elsif (@range_parts >= 2) { if (@range_parts > 2) { if (@range_parts % 2) { die "The \":\" operator expects a range sparated by a single dash: @range_parts ." . "\n"; } else { my $half = (@range_parts)/2; $a = join("-",@range_parts[0..($half-1)]); $b = join("-",@range_parts[$half..$#range_parts]); } } elsif (@range_parts == 2) { ($a,$b) = @range_parts; } else { die 'The ":" operator expects a range sparated by a dash.' . "\n"; } $key = $fdata->[0] . " between"; $value = [$a, $b]; $rule_filter = [$fdata->[0], "between", [$a, $b] ]; } else { die 'The ":" operator expects a range sparated by a dash, or a slash-separated list.' . "\n"; } } # this accounts for cases where value is null elsif (length($fdata->[2])==0) { if ($fdata->[1] eq "=") { $key = $fdata->[0]; $value = undef; $rule_filter = [ $fdata->[0], "=", undef ]; } else { $key = $fdata->[0] . " !="; $value = undef; $rule_filter = [ $fdata->[0], "!=", undef ]; } } else { $key = $fdata->[0] . ($fdata->[1] and $fdata->[1] ne '='? ' ' . $fdata->[1] : ''); $value = $fdata->[2]; $rule_filter = [ @$fdata ]; } push @keys, $key; push @values, $value; } if ($usage_hints or $order or $page) { # todo: incorporate hints in a smarter way my %p; for my $key (@keys) { $p{$key} = shift @values; } return $class->resolve( $subject_class_name, %p, ($usage_hints ? (-hints => $usage_hints) : () ), ($order ? (-order => $order) : () ), ($page ? (-page => $page) : () ), ); } else { return UR::BoolExpr->_resolve_from_subject_class_name_keys_and_values( subject_class_name => $subject_class_name, keys => \@keys, values=> \@values, ); } } sub _resolve_from_subject_class_name_keys_and_values { my $class = shift; my %params = @_; my $subject_class_name = $params{subject_class_name}; my @values = @{ $params{values} || [] }; my @constant_values = @{ $params{constant_values} || [] }; my @keys = @{ $params{keys} || [] }; die "unexpected params: " . Data::Dumper::Dumper(\%params) if %params; my $value_id = UR::BoolExpr::Util::values_to_value_id(@values); my $constant_value_id = UR::BoolExpr::Util::values_to_value_id(@constant_values); my $template_id = $subject_class_name . '/And/' . join(",",@keys) . "/" . $constant_value_id; my $rule_id = join($UR::BoolExpr::Util::id_sep,$template_id,$value_id); my $rule = __PACKAGE__->get($rule_id); $rule->{values} = \@values; return $rule; } 1; =pod =head1 NAME UR::BoolExpr - a "where clause" for objects =head1 SYNOPSIS my $o = Acme::Employee->create( ssn => '123-45-6789', name => 'Pat Jones', status => 'active', start_date => UR::Context->current->now, payroll_category => 'hourly', boss => $other_employee, ); my $bx = Acme::Employee->define_boolexpr( 'payroll_category' => 'hourly', 'status' => ['active','terminated'], 'name like' => '%Jones', 'ssn matches' => '\d{3}-\d{2}-\d{4}', 'start_date between' => ['2009-01-01','2009-02-01'], 'boss.name in' => ['Cletus Titus', 'Mitzy Mayhem'], ); $bx->evaluate($o); # true $bx->specifies_value_for('payroll_category') # true $bx->value_for('payroll_cagtegory') # 'hourly' $o->payroll_category('salary'); $bx->evaluate($o); # false # these could take either a boolean expression, or a list of params # from which it will generate one on-the-fly my $set = Acme::Employee->define_set($bx); # same as listing all of the params my @matches = Acme::Employee->get($bx); # same as above, but returns the members my $bx2 = $bx->reframe('boss'); #'employees.payroll_category' => 'hourly', #'employees.status' => ['active','terminated'], #'employees.name like' => '%Jones', #'employees.ssn matches' => '\d{3}-\d{2}-\d{4}', #'employees.start_date between' => ['2009-01-01','2009-02-01'], #'name in' => ['Cletus Titus', 'Mitzy Mayhem'], my $bx3 = $bx->flatten(); # any indirection in the params takes the form a.b.c at the lowest level # also 'payroll_category' might become 'pay_history.category', and 'pay_history.is_current' => 1 is added to the list # if this parameter has that as a custom filter =head1 DESCRIPTION A UR::BoolExpr object captures a set of match criteria for some class of object. Calls to get(), create(), and define_set() all use this internally to objectify their parameters. If given a boolean expression object directly they will use it. Otherwise they will construct one from the parameters given. They have a 1:1 correspondence within the WHERE clause in an SQL statement where RDBMS persistance is used. They also imply the FROM clause in these cases, since the query properties control which joins must be included to return the matching object set. =head1 REFLECTION The data used to create the boolean expression can be re-extracted: my $c = $r->subject_class_name; # $c eq "GSC::Clone" my @p = $r->params_list; # @p = four items my %p = $r->params_list; # %p = two key value pairs =head1 TEMPLATE SUBCLASSES The template behind the expression can be of type ::Or, ::And or ::PropertyComparison. These classes handle all of the operating logic for the expressions. Each of those classes incapsulates 0..n of the next type in the list. All templates simplify to this level. See L for details. =head1 CONSTRUCTOR =over 4 my $bx = UR::BoolExpr->resolve('Some::Class', property_1 => 'value_1', ... property_n => 'value_n'); my $bx1 = Some::Class->define_boolexpr(property_1 => value_1, ... property_n => 'value_n'); my $bx2 = Some::Class->define_boolexpr('property_1 >' => 12345); my $bx3 = UR::BoolExpr->resolve_for_string( 'Some::Class', 'property_1 = value_1 and ( property_2 < value_2 or property_3 = value_3 )', ); Returns a UR::BoolExpr object that can be used to perform tests on the given class and properties. The default comparison for each property is equality. The third example shows using greater-than operator for property_1. The last example shows constructing a UR::BoolExpr from a string containing properties, operators and values joined with 'and' and 'or', with parentheses indicating precedence. =back C can parse simple and complicated expressions. A simple expression is a property name followed by an operator followed by a value. The property name can be a series of properties joined by dots (.) to indicate traversal of multiple layers of indirect properties. Values that include spaces, characters that look like operators, commas, or other special characters should be enclosed in quotes. The parser understands all the same operators the underlying C method understands: =, <, >, <=, >=, "like", "between" and "in". Operators may be prefixed by a bang (!) or the word "not" to negate the operator. The "like" operator understands the SQL wildcards % and _. Values for the "between" operator should be separated by a minus (-). Values for the "in" operator should begin with a left bracket, end with a right bracket, and have commas between them. For example: name_property in [Bob,Fred,Joe] Simple expressions may be joined together with the words "and" and "or" to form a more complicated expression. "and" has higher precedence than "or", and parentheses can surround sub-expressions to indicate the requested precedence. For example: ((prop1 = foo or prop2 = 1) and (prop2 > 10 or prop3 like 'Yo%')) or prop4 in [1,2,3] In general, whitespace is insignificant. The strings "prop1 = 1" is parsed the same as "prop1=1". Spaces inside quoted value strings are preserved. For backward compatibility with the deprecated string parser, bare words that appear after the operators =,<,>,<= and >= which are separated by one or more spaces is treated as if it had quotes around the list of words starting with the first character of the first word and ending with the last character of the last word, meaning that spaces at the start and end of the list are trimmed. Specific ordering may be requested by putting an "order by" clause at the end, and is the same as using a -order argument to resolve(): score > 10 order by name,score. Likewise, grouping and Set construction is indicated with a "group by" clause: score > 10 group by color =head1 METHODS =over 4 =item evaluate $bx->evaluate($object) Returns true if the given object satisfies the BoolExpr =item template_and_values ($template, @values) = $bx->template_and_values(); Returns the UR::BoolExpr::Template and list of the values for the given BoolExpr =item is_subset_of $bx->is_subset_of($other_bx) Returns true if the set of objects that matches this BoolExpr is a subset of the set of objects that matches $other_bx. In practice this means: * The subject class of $bx isa the subject class of $other_bx * all the properties from $bx also appear in $other_bx * the operators and values for $bx's properties match $other_bx =item values @values = $bx->values Return a list of the values from $bx. The values will be in the same order the BoolExpr was created from =item value_for_id $id = $bx->value_for_id If $bx's properties include all the ID properties of its subject class, C returns that value. Otherwise, it returns the empty list. If the subject class has more than one ID property, this returns the value of the composite ID. =item specifies_value_for $bx->specifies_value_for('property_name'); Returns true if the filter list of $bx includes the given property name =item value_for my $value = $bx->value_for('property_name'); Return the value for the given property =item operator_for my $operator = $bx->operator_for('property_name'); Return a string for the operator of the given property. A value of '' (the empty string) means equality ("="). Other possible values inclue '<', '>', '<=', '>=', 'between', 'true', 'false', 'in', 'not <', 'not >', etc. =item normalize $bx2 = $bx->normalize; A boolen expression can be changed in incidental ways and still be equivalent. This method converts the expression into a normalized form so that it can be compared to other normalized expressions without incidental differences affecting the comparision. =item flatten $bx2 = $bx->flatten(); Transforms a boolean expression into a functional equivalent where indirect properties are turned into property chains. For instance, in a class with a => { is => "A", id_by => "a_id" }, b => { via => "a", to => "bb" }, c => { via => "b", to => "cc" }, An expression of: c => 1234 Becomes: a.bb.cc => 1234 In cases where one of the indirect properties includes a "where" clause, the flattened expression would have an additional value for each element: a => { is => "A", id_by => "a_id" }, b => { via => "a", to => "bb" }, c => { via => "b", where ["xx" => 5678], to => "cc" }, An expression of: c => 1234 Becomes: a.bb.cc => 1234 a.bb.xx => 5678 =item reframe $bx = Acme::Order->define_boolexpr(status => 'active'); $bx2 = $bx->reframe('customer'); The above will turn a query for orders which are active into a query for customers with active orders, presuming an Acme::Order has a property called "customer" with a defined relationship to another class. =back =head1 INTERNAL STRUCTURE A boolean expression (or "rule") has an "id", which completely describes the rule in stringified form, and a method called evaluate($o) which tests the rule on a given object. The id is composed of two parts: - A template_id. - A value_id. Nearly all real work delegates to the template to avoid duplication of cached details. The template_id embeds several other properties, for which the rule delegates to it: - subject_class_name, objects of which the rule can be applied-to - subclass_name, the subclass of rule (property comparison, and, or "or") - the body of the rule either key-op-val, or a list of other rules For example, the rule GSC::Clone name=x,chromosome>y: - the template_id embeds: subject_class_name = GSC::Clone subclass_name = UR::BoolExpr::And and the key-op pairs in sorted order: "chromosome>,name=" - the value_id embeds the x,y values in a special format =head1 EXAMPLES my $bool = $x->evaluate($obj); my $t = GSC::Clone->template_for_params( "status =", "chromosome []", "clone_name like", "clone_size between" ); my @results = $t->get_matching_objects( "active", [2,4,7], "Foo%", [100000,200000] ); my $r = $t->get_rule($v1,$v2,$v3); my $t = $r->template; my @results = $t->get_matching_objects($v1,$v2,$v3); my @results = $r->get_matching_objects(); @r = $r->underlying_rules(); for (@r) { print $r->evaluate($c1); } my $rt = $r->template(); my @rt = $rt->get_underlying_rule_templates(); $r = $rt->get_rule_for_values(@v); $r = UR::BoolExpr->resolve_for_string( 'My::Class', 'name=Bob and (score=10 or score < 5)', ); =head1 SEE ALSO UR(3), UR::Object(3), UR::Object::Set(3), UR::BoolExpr::Template(3) =cut BxParser.pm100664023532023421 12003212544604516 16605 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr#################################################################### # # This file was generated using Parse::Yapp version 1.05. # # Don't edit this file, use source file instead. # # ANY CHANGE MADE HERE WILL BE LOST ! # #################################################################### package UR::BoolExpr::BxParser; use vars qw ( @ISA ); use strict; @ISA= qw ( UR::BoolExpr::BxParser::Yapp::Driver ); #Included Parse/Yapp/Driver.pm file---------------------------------------- { # # Module UR::BoolExpr::BxParser::Yapp::Driver # # This module is part of the Parse::Yapp package available on your # nearest CPAN # # Any use of this module in a standalone parser make the included # text under the same copyright as the Parse::Yapp module itself. # # This notice should remain unchanged. # # (c) Copyright 1998-2001 Francois Desarmenien, all rights reserved. # (see the pod text in Parse::Yapp module for use and distribution rights) # package UR::BoolExpr::BxParser::Yapp::Driver; require 5.004; use strict; use vars qw ( $VERSION $COMPATIBLE $FILENAME ); $VERSION = '1.05'; # No BumpVersion $COMPATIBLE = '0.07'; $FILENAME=__FILE__; use Carp; #Known parameters, all starting with YY (leading YY will be discarded) my(%params)=(YYLEX => 'CODE', 'YYERROR' => 'CODE', YYVERSION => '', YYRULES => 'ARRAY', YYSTATES => 'ARRAY', YYDEBUG => ''); #Mandatory parameters my(@params)=('LEX','RULES','STATES'); sub new { my($class)=shift; my($errst,$nberr,$token,$value,$check,$dotpos); my($self)={ ERROR => \&_Error, ERRST => \$errst, NBERR => \$nberr, TOKEN => \$token, VALUE => \$value, DOTPOS => \$dotpos, STACK => [], DEBUG => 0, CHECK => \$check }; _CheckParams( [], \%params, \@_, $self ); exists($$self{VERSION}) and $$self{VERSION} < $COMPATIBLE and croak "Yapp driver version $VERSION ". "incompatible with version $$self{VERSION}:\n". "Please recompile parser module."; ref($class) and $class=ref($class); bless($self,$class); } sub YYParse { my($self)=shift; my($retval); _CheckParams( \@params, \%params, \@_, $self ); if($$self{DEBUG}) { _DBLoad(); $retval = eval '$self->_DBParse()';#Do not create stab entry on compile $@ and die $@; } else { $retval = $self->_Parse(); } $retval } sub YYData { my($self)=shift; exists($$self{USER}) or $$self{USER}={}; $$self{USER}; } sub YYErrok { my($self)=shift; ${$$self{ERRST}}=0; undef; } sub YYNberr { my($self)=shift; ${$$self{NBERR}}; } sub YYRecovering { my($self)=shift; ${$$self{ERRST}} != 0; } sub YYAbort { my($self)=shift; ${$$self{CHECK}}='ABORT'; undef; } sub YYAccept { my($self)=shift; ${$$self{CHECK}}='ACCEPT'; undef; } sub YYError { my($self)=shift; ${$$self{CHECK}}='ERROR'; undef; } sub YYSemval { my($self)=shift; my($index)= $_[0] - ${$$self{DOTPOS}} - 1; $index < 0 and -$index <= @{$$self{STACK}} and return $$self{STACK}[$index][1]; undef; #Invalid index } sub YYCurtok { my($self)=shift; @_ and ${$$self{TOKEN}}=$_[0]; ${$$self{TOKEN}}; } sub YYCurval { my($self)=shift; @_ and ${$$self{VALUE}}=$_[0]; ${$$self{VALUE}}; } sub YYExpect { my($self)=shift; keys %{$self->{STATES}[$self->{STACK}[-1][0]]{ACTIONS}} } sub YYLexer { my($self)=shift; $$self{LEX}; } ################# # Private stuff # ################# sub _CheckParams { my($mandatory,$checklist,$inarray,$outhash)=@_; my($prm,$value); my($prmlst)={}; while(($prm,$value)=splice(@$inarray,0,2)) { $prm=uc($prm); exists($$checklist{$prm}) or croak("Unknow parameter '$prm'"); ref($value) eq $$checklist{$prm} or croak("Invalid value for parameter '$prm'"); $prm=unpack('@2A*',$prm); $$outhash{$prm}=$value; } for (@$mandatory) { exists($$outhash{$_}) or croak("Missing mandatory parameter '".lc($_)."'"); } } sub _Error { print "Parse error.\n"; } sub _DBLoad { { no strict 'refs'; exists(${__PACKAGE__.'::'}{_DBParse})#Already loaded ? and return; } my($fname)=__FILE__; my(@drv); open(DRV,"<$fname") or die "Report this as a BUG: Cannot open $fname"; while() { /^\s*sub\s+_Parse\s*{\s*$/ .. /^\s*}\s*#\s*_Parse\s*$/ and do { s/^#DBG>//; push(@drv,$_); } } close(DRV); $drv[0]=~s/_P/_DBP/; eval join('',@drv); } #Note that for loading debugging version of the driver, #this file will be parsed from 'sub _Parse' up to '}#_Parse' inclusive. #So, DO NOT remove comment at end of sub !!! sub _Parse { my($self)=shift; my($rules,$states,$lex,$error) = @$self{ 'RULES', 'STATES', 'LEX', 'ERROR' }; my($errstatus,$nberror,$token,$value,$stack,$check,$dotpos) = @$self{ 'ERRST', 'NBERR', 'TOKEN', 'VALUE', 'STACK', 'CHECK', 'DOTPOS' }; #DBG> my($debug)=$$self{DEBUG}; #DBG> my($dbgerror)=0; #DBG> my($ShowCurToken) = sub { #DBG> my($tok)='>'; #DBG> for (split('',$$token)) { #DBG> $tok.= (ord($_) < 32 or ord($_) > 126) #DBG> ? sprintf('<%02X>',ord($_)) #DBG> : $_; #DBG> } #DBG> $tok.='<'; #DBG> }; $$errstatus=0; $$nberror=0; ($$token,$$value)=(undef,undef); @$stack=( [ 0, undef ] ); $$check=''; while(1) { my($actions,$act,$stateno); $stateno=$$stack[-1][0]; $actions=$$states[$stateno]; #DBG> print STDERR ('-' x 40),"\n"; #DBG> $debug & 0x2 #DBG> and print STDERR "In state $stateno:\n"; #DBG> $debug & 0x08 #DBG> and print STDERR "Stack:[". #DBG> join(',',map { $$_[0] } @$stack). #DBG> "]\n"; if (exists($$actions{ACTIONS})) { defined($$token) or do { ($$token,$$value)=&$lex($self); #DBG> $debug & 0x01 #DBG> and print STDERR "Need token. Got ".&$ShowCurToken."\n"; }; $act= exists($$actions{ACTIONS}{$$token}) ? $$actions{ACTIONS}{$$token} : exists($$actions{DEFAULT}) ? $$actions{DEFAULT} : undef; } else { $act=$$actions{DEFAULT}; #DBG> $debug & 0x01 #DBG> and print STDERR "Don't need token.\n"; } defined($act) and do { $act > 0 and do { #shift #DBG> $debug & 0x04 #DBG> and print STDERR "Shift and go to state $act.\n"; $$errstatus and do { --$$errstatus; #DBG> $debug & 0x10 #DBG> and $dbgerror #DBG> and $$errstatus == 0 #DBG> and do { #DBG> print STDERR "**End of Error recovery.\n"; #DBG> $dbgerror=0; #DBG> }; }; push(@$stack,[ $act, $$value ]); $$token ne '' #Don't eat the eof and $$token=$$value=undef; next; }; #reduce my($lhs,$len,$code,@sempar,$semval); ($lhs,$len,$code)=@{$$rules[-$act]}; #DBG> $debug & 0x04 #DBG> and $act #DBG> and print STDERR "Reduce using rule ".-$act." ($lhs,$len): "; $act or $self->YYAccept(); $$dotpos=$len; unpack('A1',$lhs) eq '@' #In line rule and do { $lhs =~ /^\@[0-9]+\-([0-9]+)$/ or die "In line rule name '$lhs' ill formed: ". "report it as a BUG.\n"; $$dotpos = $1; }; @sempar = $$dotpos ? map { $$_[1] } @$stack[ -$$dotpos .. -1 ] : (); $semval = $code ? &$code( $self, @sempar ) : @sempar ? $sempar[0] : undef; splice(@$stack,-$len,$len); $$check eq 'ACCEPT' and do { #DBG> $debug & 0x04 #DBG> and print STDERR "Accept.\n"; return($semval); }; $$check eq 'ABORT' and do { #DBG> $debug & 0x04 #DBG> and print STDERR "Abort.\n"; return(undef); }; #DBG> $debug & 0x04 #DBG> and print STDERR "Back to state $$stack[-1][0], then "; $$check eq 'ERROR' or do { #DBG> $debug & 0x04 #DBG> and print STDERR #DBG> "go to state $$states[$$stack[-1][0]]{GOTOS}{$lhs}.\n"; #DBG> $debug & 0x10 #DBG> and $dbgerror #DBG> and $$errstatus == 0 #DBG> and do { #DBG> print STDERR "**End of Error recovery.\n"; #DBG> $dbgerror=0; #DBG> }; push(@$stack, [ $$states[$$stack[-1][0]]{GOTOS}{$lhs}, $semval ]); $$check=''; next; }; #DBG> $debug & 0x04 #DBG> and print STDERR "Forced Error recovery.\n"; $$check=''; }; #Error $$errstatus or do { $$errstatus = 1; &$error($self); $$errstatus # if 0, then YYErrok has been called or next; # so continue parsing #DBG> $debug & 0x10 #DBG> and do { #DBG> print STDERR "**Entering Error recovery.\n"; #DBG> ++$dbgerror; #DBG> }; ++$$nberror; }; $$errstatus == 3 #The next token is not valid: discard it and do { $$token eq '' # End of input: no hope and do { #DBG> $debug & 0x10 #DBG> and print STDERR "**At eof: aborting.\n"; return(undef); }; #DBG> $debug & 0x10 #DBG> and print STDERR "**Dicard invalid token ".&$ShowCurToken.".\n"; $$token=$$value=undef; }; $$errstatus=3; while( @$stack and ( not exists($$states[$$stack[-1][0]]{ACTIONS}) or not exists($$states[$$stack[-1][0]]{ACTIONS}{error}) or $$states[$$stack[-1][0]]{ACTIONS}{error} <= 0)) { #DBG> $debug & 0x10 #DBG> and print STDERR "**Pop state $$stack[-1][0].\n"; pop(@$stack); } @$stack or do { #DBG> $debug & 0x10 #DBG> and print STDERR "**No state left on stack: aborting.\n"; return(undef); }; #shift the error token #DBG> $debug & 0x10 #DBG> and print STDERR "**Shift \$error token and go to state ". #DBG> $$states[$$stack[-1][0]]{ACTIONS}{error}. #DBG> ".\n"; push(@$stack, [ $$states[$$stack[-1][0]]{ACTIONS}{error}, undef ]); } #never reached croak("Error in driver logic. Please, report it as a BUG"); }#_Parse #DO NOT remove comment 1; } #End of include-------------------------------------------------- sub new { my($class)=shift; ref($class) and $class=ref($class); my($self)=$class->SUPER::new( yyversion => '1.05', yystates => [ {#State 0 ACTIONS => { 'LEFT_PAREN' => 4, 'BETWEEN_WORD' => 11, 'ASC_WORD' => 1, 'DESC_WORD' => 2, 'IDENTIFIER' => 3, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, DEFAULT => -1, GOTOS => { 'boolexpr' => 5, 'expr' => 10, 'keyword_as_value' => 7, 'property' => 15, 'condition' => 14 } }, {#State 1 DEFAULT => -66 }, {#State 2 DEFAULT => -65 }, {#State 3 DEFAULT => -29 }, {#State 4 ACTIONS => { 'ASC_WORD' => 1, 'LEFT_PAREN' => 4, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 16, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 5 ACTIONS => { '' => 17, 'OFFSET' => 18, 'ORDER_BY' => 20, 'GROUP_BY' => 19, 'LIMIT' => 21 } }, {#State 6 DEFAULT => -64 }, {#State 7 DEFAULT => -30 }, {#State 8 DEFAULT => -62 }, {#State 9 DEFAULT => -68 }, {#State 10 ACTIONS => { 'AND' => 22, 'OR' => 23 }, DEFAULT => -2 }, {#State 11 DEFAULT => -63 }, {#State 12 DEFAULT => -67 }, {#State 13 DEFAULT => -61 }, {#State 14 DEFAULT => -7 }, {#State 15 ACTIONS => { 'DOUBLEEQUAL_SIGN' => 24, 'NOT_BANG' => 33, 'NOT_WORD' => 34, 'COLON' => 25, 'LIKE_WORD' => 35, 'OPERATORS' => 26, 'EQUAL_SIGN' => 36, 'FALSE_WORD' => 37, 'BETWEEN_WORD' => 29, 'TILDE' => 38, 'TRUE_WORD' => 40, 'IN_WORD' => 41, 'IS_NOT_NULL' => 44, 'IS_NULL' => 32 }, GOTOS => { 'operator' => 28, 'like_operator' => 30, 'an_operator' => 31, 'in_operator' => 39, 'null_op_word' => 27, 'boolean_op_word' => 43, 'between_operator' => 42, 'negation' => 45 } }, {#State 16 ACTIONS => { 'AND' => 22, 'OR' => 23, 'RIGHT_PAREN' => 46 } }, {#State 17 DEFAULT => 0 }, {#State 18 ACTIONS => { 'INTEGER' => 47 } }, {#State 19 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'group_by_list' => 48, 'property' => 49 } }, {#State 20 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'MINUS' => 50, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'order_by_list' => 53, 'order_by_property' => 51, 'keyword_as_value' => 7, 'property' => 52 } }, {#State 21 ACTIONS => { 'INTEGER' => 54 } }, {#State 22 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'LEFT_PAREN' => 4, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 55, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 23 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'LEFT_PAREN' => 4, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 56, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 24 DEFAULT => -45 }, {#State 25 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 59, 'spaces' => 57 } }, {#State 26 DEFAULT => -43 }, {#State 27 DEFAULT => -20 }, {#State 28 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 60, 'spaces' => 57 } }, {#State 29 DEFAULT => -58 }, {#State 30 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'value' => 71, 'keyword_as_value' => 70, 'single_value' => 72, 'like_value' => 66, 'subsequent_value_part' => 65 } }, {#State 31 DEFAULT => -39 }, {#State 32 DEFAULT => -23 }, {#State 33 DEFAULT => -42 }, {#State 34 DEFAULT => -41 }, {#State 35 DEFAULT => -46 }, {#State 36 DEFAULT => -44 }, {#State 37 DEFAULT => -22 }, {#State 38 DEFAULT => -48 }, {#State 39 ACTIONS => { 'LEFT_BRACKET' => 76 }, GOTOS => { 'set' => 77 } }, {#State 40 DEFAULT => -21 }, {#State 41 DEFAULT => -51 }, {#State 42 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 78, 'number' => 64, 'keyword_as_value' => 70, 'single_value' => 79, 'subsequent_value_part' => 65 } }, {#State 43 DEFAULT => -19 }, {#State 44 DEFAULT => -25 }, {#State 45 ACTIONS => { 'DOUBLEEQUAL_SIGN' => 24, 'COLON' => 80, 'LIKE_WORD' => 84, 'OPERATORS' => 26, 'EQUAL_SIGN' => 36, 'BETWEEN_WORD' => 81, 'TILDE' => 85, 'IN_WORD' => 86, 'IS_NULL' => 83 }, GOTOS => { 'an_operator' => 82 } }, {#State 46 DEFAULT => -10 }, {#State 47 DEFAULT => -6 }, {#State 48 DEFAULT => -4 }, {#State 49 ACTIONS => { 'AND' => 87 }, DEFAULT => -37 }, {#State 50 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'property' => 88 } }, {#State 51 ACTIONS => { 'AND' => 89 }, DEFAULT => -35 }, {#State 52 ACTIONS => { 'ASC_WORD' => 90, 'DESC_WORD' => 91 }, DEFAULT => -31 }, {#State 53 DEFAULT => -3 }, {#State 54 DEFAULT => -5 }, {#State 55 ACTIONS => { 'AND' => 22 }, DEFAULT => -8 }, {#State 56 ACTIONS => { 'AND' => 22, 'OR' => 23 }, DEFAULT => -9 }, {#State 57 DEFAULT => -28 }, {#State 58 DEFAULT => -26 }, {#State 59 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 93, 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 92, 'single_value' => 94, 'subsequent_value_part' => 65 } }, {#State 60 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'value' => 95, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 61 DEFAULT => -73 }, {#State 62 DEFAULT => -74 }, {#State 63 ACTIONS => { 'INTEGER' => 97, 'REAL' => 96 } }, {#State 64 DEFAULT => -72 }, {#State 65 DEFAULT => -81 }, {#State 66 DEFAULT => -12 }, {#State 67 DEFAULT => -85 }, {#State 68 DEFAULT => -84 }, {#State 69 DEFAULT => -71 }, {#State 70 DEFAULT => -76 }, {#State 71 DEFAULT => -50 }, {#State 72 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -70, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 100, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 73 DEFAULT => -82 }, {#State 74 DEFAULT => -83 }, {#State 75 DEFAULT => -75 }, {#State 76 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'set_body' => 101, 'number' => 64, 'value' => 102, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 77 DEFAULT => -13 }, {#State 78 DEFAULT => -16 }, {#State 79 ACTIONS => { 'MINUS' => 103 } }, {#State 80 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 104, 'spaces' => 57 } }, {#State 81 DEFAULT => -59 }, {#State 82 DEFAULT => -40 }, {#State 83 DEFAULT => -24 }, {#State 84 DEFAULT => -47 }, {#State 85 DEFAULT => -49 }, {#State 86 DEFAULT => -52 }, {#State 87 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'group_by_list' => 105, 'property' => 49 } }, {#State 88 DEFAULT => -32 }, {#State 89 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'MINUS' => 50, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'order_by_list' => 106, 'order_by_property' => 51, 'keyword_as_value' => 7, 'property' => 52 } }, {#State 90 DEFAULT => -34 }, {#State 91 DEFAULT => -33 }, {#State 92 DEFAULT => -14 }, {#State 93 DEFAULT => -17 }, {#State 94 ACTIONS => { 'IN_DIVIDER' => 107, 'MINUS' => 103 } }, {#State 95 DEFAULT => -11 }, {#State 96 DEFAULT => -87 }, {#State 97 DEFAULT => -86 }, {#State 98 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -77, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 108, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 99 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -80, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 109, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 100 DEFAULT => -69 }, {#State 101 ACTIONS => { 'RIGHT_BRACKET' => 110 } }, {#State 102 ACTIONS => { 'SET_SEPARATOR' => 111 }, DEFAULT => -57 }, {#State 103 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'single_value' => 112, 'subsequent_value_part' => 65 } }, {#State 104 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 114, 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 113, 'single_value' => 94, 'subsequent_value_part' => 65 } }, {#State 105 DEFAULT => -38 }, {#State 106 DEFAULT => -36 }, {#State 107 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 115, 'single_value' => 116, 'subsequent_value_part' => 65 } }, {#State 108 DEFAULT => -78 }, {#State 109 DEFAULT => -79 }, {#State 110 DEFAULT => -55 }, {#State 111 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'set_body' => 117, 'number' => 64, 'value' => 102, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 112 DEFAULT => -60 }, {#State 113 DEFAULT => -15 }, {#State 114 DEFAULT => -18 }, {#State 115 DEFAULT => -53 }, {#State 116 ACTIONS => { 'IN_DIVIDER' => 107 }, DEFAULT => -54 }, {#State 117 DEFAULT => -56 } ], yyrules => [ [#Rule 0 '$start', 2, undef ], [#Rule 1 'boolexpr', 0, sub #line 10 "BxParser.yp" { [] } ], [#Rule 2 'boolexpr', 1, sub #line 11 "BxParser.yp" { UR::BoolExpr::BxParser->_simplify($_[1]) } ], [#Rule 3 'boolexpr', 3, sub #line 12 "BxParser.yp" { [@{$_[1]}, '-order', $_[3]] } ], [#Rule 4 'boolexpr', 3, sub #line 13 "BxParser.yp" { [@{$_[1]}, '-group', $_[3]] } ], [#Rule 5 'boolexpr', 3, sub #line 14 "BxParser.yp" { [@{$_[1]}, '-limit', $_[3]] } ], [#Rule 6 'boolexpr', 3, sub #line 15 "BxParser.yp" { [@{$_[1]}, '-offset', $_[3]] } ], [#Rule 7 'expr', 1, sub #line 18 "BxParser.yp" { $_[1] } ], [#Rule 8 'expr', 3, sub #line 19 "BxParser.yp" { UR::BoolExpr::BxParser->_and($_[1], $_[3]) } ], [#Rule 9 'expr', 3, sub #line 20 "BxParser.yp" { UR::BoolExpr::BxParser->_or($_[1], $_[3]) } ], [#Rule 10 'expr', 3, sub #line 21 "BxParser.yp" { $_[2] } ], [#Rule 11 'condition', 4, sub #line 24 "BxParser.yp" { [ "$_[1] $_[2]" => $_[4] ] } ], [#Rule 12 'condition', 3, sub #line 25 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 13 'condition', 3, sub #line 26 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 14 'condition', 4, sub #line 27 "BxParser.yp" { [ "$_[1] in" => $_[4] ] } ], [#Rule 15 'condition', 5, sub #line 28 "BxParser.yp" { [ "$_[1] $_[2] in" => $_[5] ] } ], [#Rule 16 'condition', 3, sub #line 29 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 17 'condition', 4, sub #line 30 "BxParser.yp" { [ "$_[1] between" => $_[4] ] } ], [#Rule 18 'condition', 5, sub #line 31 "BxParser.yp" { [ "$_[1] $_[2] between" => $_[5] ] } ], [#Rule 19 'condition', 2, sub #line 32 "BxParser.yp" { [ "$_[1] $_[2]" => 1 ] } ], [#Rule 20 'condition', 2, sub #line 33 "BxParser.yp" { [ "$_[1] $_[2]" => undef ] } ], [#Rule 21 'boolean_op_word', 1, sub #line 36 "BxParser.yp" { $_[1] } ], [#Rule 22 'boolean_op_word', 1, sub #line 37 "BxParser.yp" { $_[1] } ], [#Rule 23 'null_op_word', 1, sub #line 40 "BxParser.yp" { '=' } ], [#Rule 24 'null_op_word', 2, sub #line 41 "BxParser.yp" { "!=" } ], [#Rule 25 'null_op_word', 1, sub #line 42 "BxParser.yp" { '!=' } ], [#Rule 26 'spaces', 1, sub #line 45 "BxParser.yp" { $_[1] } ], [#Rule 27 'optional_spaces', 0, sub #line 48 "BxParser.yp" { undef } ], [#Rule 28 'optional_spaces', 1, sub #line 49 "BxParser.yp" { undef } ], [#Rule 29 'property', 1, sub #line 52 "BxParser.yp" { $_[1] } ], [#Rule 30 'property', 1, sub #line 53 "BxParser.yp" { $_[1] } ], [#Rule 31 'order_by_property', 1, sub #line 56 "BxParser.yp" { $_[1 ] } ], [#Rule 32 'order_by_property', 2, sub #line 57 "BxParser.yp" { '-'.$_[2] } ], [#Rule 33 'order_by_property', 2, sub #line 58 "BxParser.yp" { '-'.$_[1] } ], [#Rule 34 'order_by_property', 2, sub #line 59 "BxParser.yp" { $_[1] } ], [#Rule 35 'order_by_list', 1, sub #line 62 "BxParser.yp" { [ $_[1]] } ], [#Rule 36 'order_by_list', 3, sub #line 63 "BxParser.yp" { [$_[1], @{$_[3]}] } ], [#Rule 37 'group_by_list', 1, sub #line 66 "BxParser.yp" { [ $_[1] ] } ], [#Rule 38 'group_by_list', 3, sub #line 67 "BxParser.yp" { [$_[1], @{$_[3]}] } ], [#Rule 39 'operator', 1, sub #line 70 "BxParser.yp" { $_[1] } ], [#Rule 40 'operator', 2, sub #line 71 "BxParser.yp" { "$_[1] $_[2]" } ], [#Rule 41 'negation', 1, sub #line 74 "BxParser.yp" { 'not' } ], [#Rule 42 'negation', 1, sub #line 75 "BxParser.yp" { 'not' } ], [#Rule 43 'an_operator', 1, sub #line 78 "BxParser.yp" { $_[1] } ], [#Rule 44 'an_operator', 1, sub #line 79 "BxParser.yp" { '=' } ], [#Rule 45 'an_operator', 1, sub #line 80 "BxParser.yp" { '=' } ], [#Rule 46 'like_operator', 1, sub #line 83 "BxParser.yp" { 'like' } ], [#Rule 47 'like_operator', 2, sub #line 84 "BxParser.yp" { "$_[1] like" } ], [#Rule 48 'like_operator', 1, sub #line 85 "BxParser.yp" { 'like' } ], [#Rule 49 'like_operator', 2, sub #line 86 "BxParser.yp" { "$_[1] like" } ], [#Rule 50 'like_value', 1, sub #line 89 "BxParser.yp" { $_[1] =~ m/\%/ ? $_[1] : '%' . $_[1] . '%' } ], [#Rule 51 'in_operator', 1, sub #line 92 "BxParser.yp" { 'in' } ], [#Rule 52 'in_operator', 2, sub #line 93 "BxParser.yp" { "$_[1] in" } ], [#Rule 53 'old_syntax_in_value', 3, sub #line 96 "BxParser.yp" { [ $_[1], @{$_[3]} ] } ], [#Rule 54 'old_syntax_in_value', 3, sub #line 97 "BxParser.yp" { [ $_[1], $_[3] ] } ], [#Rule 55 'set', 3, sub #line 100 "BxParser.yp" { $_[2] } ], [#Rule 56 'set_body', 3, sub #line 103 "BxParser.yp" { [ $_[1], @{$_[3]} ] } ], [#Rule 57 'set_body', 1, sub #line 104 "BxParser.yp" { [ $_[1] ] } ], [#Rule 58 'between_operator', 1, sub #line 107 "BxParser.yp" { 'between' } ], [#Rule 59 'between_operator', 2, sub #line 108 "BxParser.yp" { "$_[1] between" } ], [#Rule 60 'between_value', 3, sub #line 111 "BxParser.yp" { [ $_[1], $_[3] ] } ], [#Rule 61 'keyword_as_value', 1, sub #line 114 "BxParser.yp" { $_[1] } ], [#Rule 62 'keyword_as_value', 1, sub #line 115 "BxParser.yp" { $_[1] } ], [#Rule 63 'keyword_as_value', 1, sub #line 116 "BxParser.yp" { $_[1] } ], [#Rule 64 'keyword_as_value', 1, sub #line 117 "BxParser.yp" { $_[1] } ], [#Rule 65 'keyword_as_value', 1, sub #line 118 "BxParser.yp" { $_[1] } ], [#Rule 66 'keyword_as_value', 1, sub #line 119 "BxParser.yp" { $_[1] } ], [#Rule 67 'keyword_as_value', 1, sub #line 120 "BxParser.yp" { $_[1] } ], [#Rule 68 'keyword_as_value', 1, sub #line 121 "BxParser.yp" { $_[1] } ], [#Rule 69 'value', 2, sub #line 124 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 70 'value', 1, sub #line 125 "BxParser.yp" { $_[1] } ], [#Rule 71 'subsequent_value_part', 1, sub #line 128 "BxParser.yp" { $_[1] } ], [#Rule 72 'subsequent_value_part', 1, sub #line 129 "BxParser.yp" { $_[1] } ], [#Rule 73 'subsequent_value_part', 1, sub #line 130 "BxParser.yp" { $_[1] } ], [#Rule 74 'subsequent_value_part', 1, sub #line 131 "BxParser.yp" { ($_[1] =~ m/^"(.*?)"$/)[0]; } ], [#Rule 75 'subsequent_value_part', 1, sub #line 132 "BxParser.yp" { ($_[1] =~ m/^'(.*?)'$/)[0]; } ], [#Rule 76 'subsequent_value_part', 1, sub #line 133 "BxParser.yp" { $_[1] } ], [#Rule 77 'subsequent_values_list', 1, sub #line 136 "BxParser.yp" { $_[1] } ], [#Rule 78 'subsequent_values_list', 2, sub #line 137 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 79 'subsequent_values_list', 2, sub #line 138 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 80 'subsequent_values_list', 1, sub #line 139 "BxParser.yp" { '' } ], [#Rule 81 'single_value', 1, sub #line 142 "BxParser.yp" { $_[1] } ], [#Rule 82 'single_value', 1, sub #line 143 "BxParser.yp" { $_[1] } ], [#Rule 83 'single_value', 1, sub #line 144 "BxParser.yp" { $_[1] } ], [#Rule 84 'number', 1, sub #line 148 "BxParser.yp" { $_[1] + 0 } ], [#Rule 85 'number', 1, sub #line 149 "BxParser.yp" { $_[1] + 0 } ], [#Rule 86 'number', 2, sub #line 150 "BxParser.yp" { 0 - $_[2] } ], [#Rule 87 'number', 2, sub #line 151 "BxParser.yp" { 0 - $_[2] } ] ], @_); bless($self,$class); } #line 154 "BxParser.yp" package UR::BoolExpr::BxParser; use strict; use warnings; sub _error { my @expect = $_[0]->YYExpect; my $tok = $_[0]->YYData->{INPUT}; my $match = $_[0]->YYData->{MATCH}; my $string = $_[0]->YYData->{STRING}; my $err = qq(Can't parse expression "$string"\n Syntax error near token $tok '$match'); my $rem = $_[0]->YYData->{REMAINING}; $err .= ", remaining text: '$rem'" if $rem; $err .= "\nExpected one of: " . join(", ", @expect) . "\n"; Carp::croak($err); } my %token_states = ( 'DEFAULT' => [ WHITESPACE => qr{\s+}, AND => [ qr{and}i, 'DEFAULT'], OR => [ qr{or}i, 'DEFAULT' ], BETWEEN_WORD => qr{between}, LIKE_WORD => qr{like}, IN_WORD => qr{in}, NOT_WORD => qr{not}, DESC_WORD => qr{desc}, ASC_WORD => qr{asc}, TRUE_WORD => qr{true}, FALSE_WORD => qr{false}, LIMIT => qr{limit}, OFFSET => qr{offset}, IDENTIFIER => qr{[a-zA-Z_][a-zA-Z0-9_.]*}, MINUS => qr{-}, INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => [ qr{[%\+\.\/\w][\+\-\.%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards 'dont_gobble_spaces' ], DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, LEFT_PAREN => [ qr{\(}, 'DEFAULT' ], RIGHT_PAREN => [ qr{\)}, 'DEFAULT' ], LEFT_BRACKET => [ qr{\[}, 'set_contents'], RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], NOT_BANG => qr{!}, EQUAL_SIGN => [ qr{=}, 'dont_gobble_spaces' ], DOUBLEEQUAL_SIGN => [ qr{=>}, 'dont_gobble_spaces' ], OPERATORS => [ qr{<=|>=|<|>}, 'dont_gobble_spaces' ], AND => [ qr{,}, 'DEFAULT' ], COLON => [ qr{:}, 'after_colon_value' ], TILDE => qr{~}, ORDER_BY => qr{order by}, GROUP_BY => qr{group by}, IS_NULL => qr{is null|is undef}, IS_NOT_NULL => qr{is not null|is not undef}, ], 'set_contents' => [ SET_SEPARATOR => qr{,}, # Depending on state, can be either AND or SET_SEPARATOR WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], ], 'after_colon_value' => [ INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, IN_DIVIDER => qr{\/}, #WORD => qr{\w+}, # Override WORD in DEFAULT to disallow / WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # Override WORD in DEFAULT to disallow / DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, WHITESPACE => [qr{\s+}, 'DEFAULT'], ], 'dont_gobble_spaces' => [ AND => [ qr{and}, 'DEFAULT'], OR => [ qr{or}, 'DEFAULT' ], LIMIT => [qr{limit}, 'DEFAULT'], OFFSET => [qr{offset}, 'DEFAULT'], INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.\:%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards ORDER_BY => [qr{order by}, 'DEFAULT'], GROUP_BY => [qr{group by}, 'DEFAULT'], ], ); sub parse { my $string = shift; my %params = @_; my $debug = $params{'tokdebug'}; my $yydebug = $params{'yydebug'} || 0; print "\nStarting parse for string $string\n" if $debug; my $parser = UR::BoolExpr::BxParser->new(); $parser->YYData->{STRING} = $string; my $parser_state = 'DEFAULT'; my $get_next_token = sub { if (length($string) == 0) { print "String is empty, we're done!\n" if $debug; return (undef, ''); } GET_NEXT_TOKEN: foreach (1) { my $longest = 0; my $longest_token = ''; my $longest_match = ''; for my $token_list ( $parser_state, 'DEFAULT' ) { print "\nTrying tokens for state $token_list...\n" if $debug; my $tokens = $token_states{$token_list}; for(my $i = 0; $i < @$tokens; $i += 2) { my($tok, $re) = @$tokens[$i, $i+1]; print "Trying token $tok... " if $debug; my($regex,$next_parser_state); if (ref($re) eq 'ARRAY') { ($regex,$next_parser_state) = @$re; } else { $regex = $re; } if ($string =~ m/^($regex)/) { print "Matched >>$1<<" if $debug; my $match_len = length($1); if ($match_len > $longest) { print "\n ** It's now the longest" if $debug; $longest = $match_len; $longest_token = $tok; $longest_match = $1; if ($next_parser_state) { $parser_state = $next_parser_state; } } } print "\n" if $debug; } $string = substr($string, $longest); print "Consuming up to char pos $longest chars, string is now >>$string<<\n" if $debug; if ($longest_token eq 'WHITESPACE' and $parser_state ne 'dont_gobble_spaces') { print "Redoing token extraction after whitespace\n" if $debug; redo GET_NEXT_TOKEN; } $parser->YYData->{REMAINING} = $string; if ($longest) { print "Returning token $longest_token, match $longest_match\n next state is named $parser_state\n" if $debug; $parser->YYData->{INPUT} = $longest_token; $parser->YYData->{MATCH} = $longest_match; return ($longest_token, $longest_match); } last if $token_list eq 'DEFAULT'; # avoid going over it twice if $parser_state is DEFAULT } } print "Didn't match anything, done!\n" if $debug; return (undef, ''); # Didn't match anything }; return ( $parser->YYParse( yylex => $get_next_token, yyerror => \&_error, yydebug => $yydebug), \$string, ); } # Used by the top-level expr production to turn an or-type parse tree with # only a single AND condition into a simple AND-type tree (1-level arrayref). # Or to add the '-or' to the front of a real OR-type tree so it can be passed # directly to UR::BoolExpr::resolve() sub _simplify { my($class, $expr) = @_; if (ref($expr->[0])) { if (@$expr == 1) { # An or-type parse tree, but with only one AND subrule - use as a simple and-type rule $expr = $expr->[0]; } else { $expr = ['-or', $expr]; # an or-type parse tree with multiple subrules } } return $expr; } # Handles the case for "expr AND expr" where one or both exprs can be an # OR-type expr. In that case, it distributes the AND exprs among all the # OR conditions. For example: # (a=1 or b=2) and (c=3 or d=4) # is the same as # (a=1 and c=3) or (a=1 and d=4) or (b=2 and c=3) or (b=2 and d=4) # This is necessary because the BoolExpr resolver can only handle 1-level deep # AND-type rules, or a 1-level deep OR-type rule composed of any number of # 1-level deep AND-type rules sub _and { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); my @and; foreach my $left_subexpr ( @$left ) { foreach my $right_subexpr (@$right) { push @and, [@$left_subexpr, @$right_subexpr]; } } \@and; } sub _or { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); [ @$left, @$right ]; } 1; 1; BxParser.yp100664023532023421 3336012544604516 16610 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr%right OR %right AND %left MINUS # Generate the perl module with the command: # yapp -sm UR::BoolExpr::BxParser -o - BxParser.yp | sed s/Parse::Yapp::Driver/UR::BoolExpr::BxParser::Yapp::Driver/ > BxParser.pm %% boolexpr: { [] } | expr { UR::BoolExpr::BxParser->_simplify($_[1]) } | boolexpr ORDER_BY order_by_list { [@{$_[1]}, '-order', $_[3]] } | boolexpr GROUP_BY group_by_list { [@{$_[1]}, '-group', $_[3]] } | boolexpr LIMIT INTEGER { [@{$_[1]}, '-limit', $_[3]] } | boolexpr OFFSET INTEGER { [@{$_[1]}, '-offset', $_[3]] } ; expr: condition { $_[1] } | expr AND expr { UR::BoolExpr::BxParser->_and($_[1], $_[3]) } | expr OR expr { UR::BoolExpr::BxParser->_or($_[1], $_[3]) } | LEFT_PAREN expr RIGHT_PAREN { $_[2] } ; condition: property operator optional_spaces value { [ "$_[1] $_[2]" => $_[4] ] } | property like_operator like_value { [ "$_[1] $_[2]" => $_[3] ] } | property in_operator set { [ "$_[1] $_[2]" => $_[3] ] } | property COLON optional_spaces old_syntax_in_value { [ "$_[1] in" => $_[4] ] } | property negation COLON optional_spaces old_syntax_in_value { [ "$_[1] $_[2] in" => $_[5] ] } | property between_operator between_value { [ "$_[1] $_[2]" => $_[3] ] } | property COLON optional_spaces between_value { [ "$_[1] between" => $_[4] ] } | property negation COLON optional_spaces between_value { [ "$_[1] $_[2] between" => $_[5] ] } | property boolean_op_word { [ "$_[1] $_[2]" => 1 ] } | property null_op_word { [ "$_[1] $_[2]" => undef ] } ; boolean_op_word: TRUE_WORD { $_[1] } | FALSE_WORD { $_[1] } ; null_op_word: IS_NULL { '=' } | negation IS_NULL { "!=" } | IS_NOT_NULL { '!=' } ; spaces: WHITESPACE { $_[1] } ; optional_spaces: { undef } | spaces { undef } ; property: IDENTIFIER { $_[1] } | keyword_as_value { $_[1] } ; order_by_property: property { $_[1 ] } | MINUS property { '-'.$_[2] } | property DESC_WORD { '-'.$_[1] } | property ASC_WORD { $_[1] } ; order_by_list: order_by_property { [ $_[1]] } | order_by_property AND order_by_list { [$_[1], @{$_[3]}] } ; group_by_list: property { [ $_[1] ] } | property AND group_by_list { [$_[1], @{$_[3]}] } ; operator: an_operator { $_[1] } | negation an_operator { "$_[1] $_[2]" } ; negation: NOT_WORD { 'not' } | NOT_BANG { 'not' } ; an_operator: OPERATORS { $_[1] } | EQUAL_SIGN { '=' } | DOUBLEEQUAL_SIGN { '=' } ; like_operator: LIKE_WORD { 'like' } | negation LIKE_WORD { "$_[1] like" } | TILDE { 'like' } | negation TILDE { "$_[1] like" } ; like_value: value { $_[1] =~ m/\%/ ? $_[1] : '%' . $_[1] . '%' } ; in_operator: IN_WORD { 'in' } | negation IN_WORD { "$_[1] in" } ; old_syntax_in_value: single_value IN_DIVIDER old_syntax_in_value { [ $_[1], @{$_[3]} ] } | single_value IN_DIVIDER single_value { [ $_[1], $_[3] ] } ; set: LEFT_BRACKET set_body RIGHT_BRACKET { $_[2] } ; set_body: value SET_SEPARATOR set_body { [ $_[1], @{$_[3]} ] } | value { [ $_[1] ] } ; between_operator: BETWEEN_WORD { 'between' } | negation BETWEEN_WORD { "$_[1] between" } ; between_value: single_value MINUS single_value { [ $_[1], $_[3] ] } ; keyword_as_value: IN_WORD { $_[1] } | LIKE_WORD { $_[1] } | BETWEEN_WORD { $_[1] } | NOT_WORD { $_[1] } | DESC_WORD { $_[1] } | ASC_WORD { $_[1] } | TRUE_WORD { $_[1] } | FALSE_WORD { $_[1] } ; value: single_value subsequent_values_list { $_[1].$_[2] } | single_value { $_[1] } ; subsequent_value_part: IDENTIFIER { $_[1] } | number { $_[1] } | WORD { $_[1] } | DOUBLEQUOTE_STRING { ($_[1] =~ m/^"(.*?)"$/)[0]; } | SINGLEQUOTE_STRING { ($_[1] =~ m/^'(.*?)'$/)[0]; } | keyword_as_value { $_[1] } ; subsequent_values_list: subsequent_value_part { $_[1] } | subsequent_value_part subsequent_values_list { $_[1].$_[2] } | spaces subsequent_values_list { $_[1].$_[2] } | spaces { '' } # to gobble the final space in a value before the next expression part ; single_value: subsequent_value_part { $_[1] } | AND { $_[1] } | OR { $_[1] } ; number: INTEGER { $_[1] + 0 } | REAL { $_[1] + 0 } | MINUS INTEGER { 0 - $_[2] } # to reject --5 | MINUS REAL { 0 - $_[2] } ; %% package UR::BoolExpr::BxParser; use strict; use warnings; sub _error { my @expect = $_[0]->YYExpect; my $tok = $_[0]->YYData->{INPUT}; my $match = $_[0]->YYData->{MATCH}; my $string = $_[0]->YYData->{STRING}; my $err = qq(Can't parse expression "$string"\n Syntax error near token $tok '$match'); my $rem = $_[0]->YYData->{REMAINING}; $err .= ", remaining text: '$rem'" if $rem; $err .= "\nExpected one of: " . join(", ", @expect) . "\n"; Carp::croak($err); } my %token_states = ( 'DEFAULT' => [ WHITESPACE => qr{\s+}, AND => [ qr{and}i, 'DEFAULT'], OR => [ qr{or}i, 'DEFAULT' ], BETWEEN_WORD => qr{between}, LIKE_WORD => qr{like}, IN_WORD => qr{in}, NOT_WORD => qr{not}, DESC_WORD => qr{desc}, ASC_WORD => qr{asc}, TRUE_WORD => qr{true}, FALSE_WORD => qr{false}, LIMIT => qr{limit}, OFFSET => qr{offset}, IDENTIFIER => qr{[a-zA-Z_][a-zA-Z0-9_.]*}, MINUS => qr{-}, INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => [ qr{[%\+\.\/\w][\+\-\.%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards 'dont_gobble_spaces' ], DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, LEFT_PAREN => [ qr{\(}, 'DEFAULT' ], RIGHT_PAREN => [ qr{\)}, 'DEFAULT' ], LEFT_BRACKET => [ qr{\[}, 'set_contents'], RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], NOT_BANG => qr{!}, EQUAL_SIGN => [ qr{=}, 'dont_gobble_spaces' ], DOUBLEEQUAL_SIGN => [ qr{=>}, 'dont_gobble_spaces' ], OPERATORS => [ qr{<=|>=|<|>}, 'dont_gobble_spaces' ], AND => [ qr{,}, 'DEFAULT' ], COLON => [ qr{:}, 'after_colon_value' ], TILDE => qr{~}, ORDER_BY => qr{order by}, GROUP_BY => qr{group by}, IS_NULL => qr{is null|is undef}, IS_NOT_NULL => qr{is not null|is not undef}, ], 'set_contents' => [ SET_SEPARATOR => qr{,}, # Depending on state, can be either AND or SET_SEPARATOR WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], ], 'after_colon_value' => [ INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, IN_DIVIDER => qr{\/}, #WORD => qr{\w+}, # Override WORD in DEFAULT to disallow / WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # Override WORD in DEFAULT to disallow / DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, WHITESPACE => [qr{\s+}, 'DEFAULT'], ], 'dont_gobble_spaces' => [ AND => [ qr{and}, 'DEFAULT'], OR => [ qr{or}, 'DEFAULT' ], LIMIT => [qr{limit}, 'DEFAULT'], OFFSET => [qr{offset}, 'DEFAULT'], INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.\:%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards ORDER_BY => [qr{order by}, 'DEFAULT'], GROUP_BY => [qr{group by}, 'DEFAULT'], ], ); sub parse { my $string = shift; my %params = @_; my $debug = $params{'tokdebug'}; my $yydebug = $params{'yydebug'} || 0; print "\nStarting parse for string $string\n" if $debug; my $parser = UR::BoolExpr::BxParser->new(); $parser->YYData->{STRING} = $string; my $parser_state = 'DEFAULT'; my $get_next_token = sub { if (length($string) == 0) { print "String is empty, we're done!\n" if $debug; return (undef, ''); } GET_NEXT_TOKEN: foreach (1) { my $longest = 0; my $longest_token = ''; my $longest_match = ''; for my $token_list ( $parser_state, 'DEFAULT' ) { print "\nTrying tokens for state $token_list...\n" if $debug; my $tokens = $token_states{$token_list}; for(my $i = 0; $i < @$tokens; $i += 2) { my($tok, $re) = @$tokens[$i, $i+1]; print "Trying token $tok... " if $debug; my($regex,$next_parser_state); if (ref($re) eq 'ARRAY') { ($regex,$next_parser_state) = @$re; } else { $regex = $re; } if ($string =~ m/^($regex)/) { print "Matched >>$1<<" if $debug; my $match_len = length($1); if ($match_len > $longest) { print "\n ** It's now the longest" if $debug; $longest = $match_len; $longest_token = $tok; $longest_match = $1; if ($next_parser_state) { $parser_state = $next_parser_state; } } } print "\n" if $debug; } $string = substr($string, $longest); print "Consuming up to char pos $longest chars, string is now >>$string<<\n" if $debug; if ($longest_token eq 'WHITESPACE' and $parser_state ne 'dont_gobble_spaces') { print "Redoing token extraction after whitespace\n" if $debug; redo GET_NEXT_TOKEN; } $parser->YYData->{REMAINING} = $string; if ($longest) { print "Returning token $longest_token, match $longest_match\n next state is named $parser_state\n" if $debug; $parser->YYData->{INPUT} = $longest_token; $parser->YYData->{MATCH} = $longest_match; return ($longest_token, $longest_match); } last if $token_list eq 'DEFAULT'; # avoid going over it twice if $parser_state is DEFAULT } } print "Didn't match anything, done!\n" if $debug; return (undef, ''); # Didn't match anything }; return ( $parser->YYParse( yylex => $get_next_token, yyerror => \&_error, yydebug => $yydebug), \$string, ); } # Used by the top-level expr production to turn an or-type parse tree with # only a single AND condition into a simple AND-type tree (1-level arrayref). # Or to add the '-or' to the front of a real OR-type tree so it can be passed # directly to UR::BoolExpr::resolve() sub _simplify { my($class, $expr) = @_; if (ref($expr->[0])) { if (@$expr == 1) { # An or-type parse tree, but with only one AND subrule - use as a simple and-type rule $expr = $expr->[0]; } else { $expr = ['-or', $expr]; # an or-type parse tree with multiple subrules } } return $expr; } # Handles the case for "expr AND expr" where one or both exprs can be an # OR-type expr. In that case, it distributes the AND exprs among all the # OR conditions. For example: # (a=1 or b=2) and (c=3 or d=4) # is the same as # (a=1 and c=3) or (a=1 and d=4) or (b=2 and c=3) or (b=2 and d=4) # This is necessary because the BoolExpr resolver can only handle 1-level deep # AND-type rules, or a 1-level deep OR-type rule composed of any number of # 1-level deep AND-type rules sub _and { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); my @and; foreach my $left_subexpr ( @$left ) { foreach my $right_subexpr (@$right) { push @and, [@$left_subexpr, @$right_subexpr]; } } \@and; } sub _or { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); [ @$left, @$right ]; } 1; Template.pm100664023532023421 5502512544604516 16623 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr =head1 NAME UR::BoolExpr::Template - a UR::BoolExpr minus specific values =head1 SYNOPSIS =head1 DESCRIPTION =cut package UR::BoolExpr::Template; use warnings; use strict; use Scalar::Util qw(blessed); use Data::Dumper; use UR; our @CARP_NOT = qw(UR::BoolExpr); # readable stringification use overload ('""' => 'id'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); UR::Object::Type->define( class_name => __PACKAGE__, is_transactional => 0, composite_id_separator => '/', id_by => [ subject_class_name => { is => 'Text' }, logic_type => { is => 'Text' }, logic_detail => { is => 'Text' }, constant_value_id => { is => 'Text' } ], has => [ is_normalized => { is => 'Boolean' }, is_id_only => { is => 'Boolean' }, is_partial_id => { is => 'Boolean' }, # True if at least 1, but not all the ID props are mentioned is_unique => { is => 'Boolean' }, matches_all => { is => 'Boolean' }, key_op_hash => { is => 'HASH' }, id_position => { is => 'Integer' }, normalized_id => { is => 'Text' }, normalized_positions_arrayref => { is => 'ARRAY' }, normalization_extender_arrayref => { is => 'ARRAY' }, _property_meta_hash => { is => 'HASH' }, _property_names_arrayref => { is => 'ARRAY' }, num_values => { is => 'Integer' }, _ambiguous_keys => { is => 'ARRAY' }, _keys => { is => 'ARRAY' }, _constant_values => { is => 'ARRAY' }, ], has_optional => [ hints => { is => 'ARRAY' }, recursion_desc => { is => 'ARRAY' }, order_by => { is => 'ARRAY' }, group_by => { is => 'ARRAY' }, aggregate => { is => 'ARRAY' }, limit => { is => 'Integer' }, offset => { is => 'Integer' }, ] ); our $VERSION = "0.44"; # UR $VERSION;; # Borrow from the util package. # This will go away with refactoring. our $id_sep = $UR::BoolExpr::Util::id_sep; our $record_sep = $UR::BoolExpr::Util::record_sep; our $unit_sep = $UR::BoolExpr::Util::unit_sep; our $null_value = $UR::BoolExpr::Util::null_value; our $empty_string = $UR::BoolExpr::Util::empty_string; our $empty_list = $UR::BoolExpr::Util::empty_list; # Names of the optional flags you can add to a rule our @meta_param_names = qw(recursion_desc hints order_by group_by aggregate limit offset); # Wrappers for regular properties sub _property_names { return @{ $_[0]->{_property_names_arrayref} }; } # Indexability methods sub _indexable_property_names { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; @{ $_[0]->{_indexable_property_names} } } sub _indexable_property_positions { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; @{ $_[0]->{_indexable_property_positions} } } sub _is_fully_indexable { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; $_[0]->{_is_fully_indexable}; } sub _resolve_indexing_params { my $self = $_[0]; my $class_meta = UR::Object::Type->get($self->subject_class_name); my @all_names = $self->_property_names; for my $name (@all_names) { my $m = $class_meta->property($name); unless ($m) { #$DB::single = 1; $class_meta->property($name); #$DB::single = 1; $class_meta->property($name); } } my @indexable_names = sort map { $_->property_name } grep { $_ } #and $_->is_indexable } map { $class_meta->property_meta_for_name($_) } @all_names; my @indexable_positions = UR::Util::positions_of_values(\@all_names,\@indexable_names); $self->{_indexable_property_names} = \@indexable_names; $self->{_indexable_property_positions} = \@indexable_positions; $self->{_is_fully_indexable} = (@indexable_names == @all_names); return 1; } # Return true if this rule template's parameters is a subset of the other's parameters # Returns 0 if this rule specifies a parameter not in the other template # Returns undef if all the properties match, but their operators do not, meaning that # we do not know if an object evaluated as true under one rule's template would also be in the other sub is_subset_of { my($self,$other_template) = @_; my $other_template_id = $other_template->id; my $cached_subset_data = $self->{'__cache'}->{'is_subset_of'} ||= {}; if (exists $cached_subset_data->{$other_template_id}) { return $cached_subset_data->{$other_template_id}; } unless (ref($other_template) and $self->isa(ref $other_template)) { $cached_subset_data->{$other_template_id} = 0; return 0; } my $my_class = $self->subject_class_name; my $other_class = $other_template->subject_class_name; unless ($my_class eq $other_class or $my_class->isa($other_class)) { $cached_subset_data->{$other_template_id} = undef; return; } my %operators = map { $_ => $self->operator_for($_) } $self->_property_names; my $operators_match = 1; foreach my $prop ( $other_template->_property_names ) { unless (exists $operators{$prop}) { $operators_match = 0; last; } $operators_match = undef if ($operators{$prop} ne $other_template->operator_for($prop)); } $cached_subset_data->{$other_template_id} = $operators_match; return $operators_match; } # This is set lazily currently sub is_unique { my $self = $_[0]; if (defined $self->{is_unique}) { return $self->{is_unique} } # since this requires normalization, we don't set the value at construction time my $normalized_self; if ($self->is_normalized) { $normalized_self = $self; } else { $normalized_self = $self->get_normalized_template_equivalent($self); } my $op = $normalized_self->operator_for('id'); if (defined($op) and ($op eq '' or $op eq '=')) { return $self->{is_unique} = 1; } else { $self->{is_unique} = 0; # if some combination of params can combine to # satisfy at least one unique constraint, # then we have uniqueness in the parameters. if (my @ps = $self->subject_class_name->__meta__->unique_property_sets) { my $property_meta_hash = $self->_property_meta_hash; for my $property_set (@ps) { my $property_set = (ref($property_set) ? $property_set : [$property_set]); my @properties_used_from_constraint = grep { defined($_) } @$property_meta_hash{@$property_set}; if (@properties_used_from_constraint == @$property_set) { # filter imprecise operators @properties_used_from_constraint = grep { $_->{operator} !~ /^(not |)like(-.|)$/i and $_->{operator} !~ /^(not |)in/i } @properties_used_from_constraint; if (@properties_used_from_constraint == @$property_set) { $self->{is_unique} = 1; last; } else { ## print "some properties use bad operators: @properties_used_from_constraint\n"; } } else { ## print "too few properties in @properties_used_from_constraint\n"; } } } return $self->{is_unique}; } } # Derivative of the ID. sub rule_template_subclass_name { return "UR::BoolExpr::Template::" . shift->logic_type; } sub get_normalized_template_equivalent { UR::BoolExpr::Template->get($_[0]->{normalized_id}); } sub get_rule_for_values { my $self = shift; my $value_id = UR::BoolExpr::Util::values_to_value_id(@_); my $rule_id = UR::BoolExpr->__meta__->resolve_composite_id_from_ordered_values($self->id,$value_id); my $r = UR::BoolExpr->get($rule_id); # # # FIXME - Don't do this part if the operator is 'in' or 'between' # for (my $i = 0; $i < @_; $i++) { # if (ref($_[$i]) and ! Scalar::Util::blessed($_[$i])) { # $r->{'hard_refs'}->{$i} = $_[$i]; # } # } return $r; } sub get_rule_for_value_id { my $self = shift; my $value_id = shift; my $rule_id = UR::BoolExpr->__meta__->resolve_composite_id_from_ordered_values($self->id,$value_id); return UR::BoolExpr->get($rule_id); } sub extend_params_list_for_values { my $self = shift; #my @prev = @_; my $extenders = $self->normalization_extender_arrayref; if (@$extenders) { my @result; my $subject_class = $self->subject_class_name->__meta__; for my $n (0 .. @$extenders-1) { my $extender = $extenders->[$n]; my ($input_positions_arrayref,$subref,@more_keys) = @$extender; my @more_values = @_[@$input_positions_arrayref]; if ($subref) { ## print "calling $subref on \n\t" . join("\n\t",@more_values) . "\n"; @more_values = $subject_class->$subref(@more_values); ## print "got: \n\t" . join("\n\t",@more_values) . "\n"; } while (@more_keys) { my $k = shift @more_keys; my $v = shift @more_values; push @result, $k => $v; } } return @result; } return (); } sub get_normalized_rule_for_values { my $self = shift; my @unnormalized_values = @_; if ($self->is_normalized) { return $self->get_rule_for_values(@unnormalized_values); } my $normalized_rule_template = $self->get_normalized_template_equivalent; # The normalized rule set may have more values than were actually # passed-in. These 'extenders' will add to the @values array # before re-ordering it. my $extenders = $self->normalization_extender_arrayref; if (@$extenders) { my $subject_class = $self->subject_class_name->__meta__; for my $extender (@$extenders) { my ($input_positions_arrayref,$subref) = @$extender; my @more_values = @unnormalized_values[@$input_positions_arrayref]; if ($subref) { ## print "calling $subref on \n\t" . join("\n\t",@more_values) . "\n"; @more_values = $subject_class->$subref(@more_values); ## print "got: \n\t" . join("\n\t",@more_values) . "\n"; } push @unnormalized_values, @more_values; } } # Normalize the values. Since the normalized template may have added properties, # and a different order we may need to re-order and expand the values list. my $normalized_positions_arrayref = $self->normalized_positions_arrayref; my @normalized_values = @unnormalized_values[@$normalized_positions_arrayref]; my $rule = $normalized_rule_template->get_rule_for_values(@normalized_values); return $rule; } sub _normalize_non_ur_values_hash { my ($self,$unnormalized) = @_; my %normalized; if ($self->subject_class_name ne 'UR::Object::Property') { my $normalized_positions_arrayref = $self->normalized_positions_arrayref; my @reordered_values = @$unnormalized{@$normalized_positions_arrayref}; for (my $n = 0; $n < @reordered_values; $n++) { my $value = $reordered_values[$n]; $normalized{$n} = $value if defined $value; } } return \%normalized; } sub value_position_for_property_name { if (exists $_[0]{_property_meta_hash}{$_[1]}) { return $_[0]{_property_meta_hash}{$_[1]}{value_position}; } else { return undef; } } sub operator_for { if (exists $_[0]{_property_meta_hash}{$_[1]}) { return $_[0]{_property_meta_hash}{$_[1]}{operator} || '='; } else { return undef; } } sub operators_for_properties { my %properties = map { $_ => $_[0]->{'_property_meta_hash'}->{$_}->{'operator'} || '=' } @{ $_[0]->{'_property_names_arrayref'} }; return \%properties; } sub add_filter { my $self = shift; my $property_name = shift; my $op = shift; my $new_key = $property_name; $new_key .= ' ' . $op if defined $op; my ($subject_class_name, $logic_type, $logic_detail) = split("/",$self->id); unless ($logic_type eq 'And') { die "Attempt to add a filter to a rule besides an 'And' rule!"; } my @keys = split(',',$logic_detail); my $new_id = join('/',$subject_class_name,$logic_type,join(',',@keys,$new_key)); return $self->class->get($new_id); } sub remove_filter { my $self = shift; my $filter = shift; my ($subject_class_name, $logic_type, $logic_detail) = split("/",$self->id); my @keys = grep { $_ !~ /^${filter}\b/ } split(',',$logic_detail); my $new_id = join('/',$subject_class_name,$logic_type,join(',',@keys)); #print "$new_id\n"; return $self->class->get($new_id); } sub sub_classify { my ($self,$subclass_name) = @_; my $new_id = $self->id; $new_id =~ s/^.*?\//$subclass_name\//; return $self->class->get($new_id); } # flyweight constructor # NOTE: this caches outside of the regular system since these are stateless objects sub get_by_subject_class_name_logic_type_and_logic_detail { my $class = shift; my $subject_class_name = shift; Carp::croak("Expected a subject class name as the first arg of UR::BoolExpr::Template constructor, got " . ( defined($subject_class_name) ? "'$subject_class_name'" : "(undef)" ) ) unless ($subject_class_name); my $logic_type = shift; my $logic_detail = shift; my $constant_value_id = shift || UR::BoolExpr::Util::values_to_value_id(); # default is an empty list of values return $class->get(join('/',$subject_class_name,$logic_type,$logic_detail,$constant_value_id)); } # The analogue of resolve in UR::BoolExpr. @params_list is a list if # strings containing properties and operators separated by a space. For ex: "some_param =" sub resolve { my($class,$subject_class_name, @params_list) = @_; my(@params, @constant_values); for (my $i = 0; $i < @params_list; $i++) { push @params, $params_list[$i]; if (UR::BoolExpr::Util::is_meta_param($params_list[$i])) { push @constant_values, $params_list[++$i]; } } return $class->get_by_subject_class_name_logic_type_and_logic_detail( $subject_class_name, "And", join(',',@params), UR::BoolExpr::Util::values_to_value_id(@constant_values)); } sub get { my $class = shift; my $id = shift; Carp::croak("Non-id params not supported for " . __PACKAGE__ . " yet!") if @_; my $self = $UR::Object::rule_templates->{$id}; return $self if $self; my ($subject_class_name,$logic_type,$logic_detail,$constant_value_id,@extra) = split('/',$id); if (@extra) { # account for a possible slash in the constant value id $constant_value_id = join('/',$constant_value_id,@extra); } # work on the base class or on subclasses my $sub_class_name = ( $class eq __PACKAGE__ ? __PACKAGE__ . "::" . $logic_type : $class ); unless ($logic_type) { Carp::croak("Could not determine logic type from UR::BoolExpr::Template with id $id"); } if ($logic_type eq "And") { # TODO: move into subclass my @keys = split(/,/,$logic_detail || ''); my @constant_values; @constant_values = UR::BoolExpr::Util::value_id_to_values($constant_value_id) if defined $constant_value_id; return $sub_class_name->_fast_construct( $subject_class_name, \@keys, \@constant_values, $logic_detail, $constant_value_id, ); } else { $self = bless { id => $id, subject_class_name => $subject_class_name, logic_type => $logic_type, logic_detail => $logic_detail, constant_value_id => $constant_value_id, normalized_id => $id, }, $sub_class_name; $UR::Object::rule_templates->{$id} = $self; return $self; } } # Return true if the template has recursion_desc, hints, order or page set sub has_meta_options { my $self = shift; return 1 if @$self{@meta_param_names}; return 0; } # This is the basis for the hash used by the existing UR::Object system for each rule. # this is created upon first request and cached in the object sub legacy_params_hash { my $self = shift; my $legacy_params_hash = $self->{legacy_params_hash}; return $legacy_params_hash if $legacy_params_hash; $legacy_params_hash = {}; my $template_id = $self->id; my $key_op_hash = $self->key_op_hash; my $id_only = $self->is_id_only; my $subject_class_name = $self->subject_class_name; my $logic_type = $self->logic_type; my $logic_detail = $self->logic_detail; my @keys_sorted = $self->_underlying_keys; my $subject_class_meta = $subject_class_name->__meta__; if ( (@keys_sorted and not $logic_detail) or ($logic_detail and not @keys_sorted) ) { Carp::confess(); } if (!$logic_detail) { %$legacy_params_hash = (_unique => 0, _none => 1); } else { # _id_only if ($id_only) { $legacy_params_hash->{_id_only} = 1; } else { $legacy_params_hash->{_id_only} = 0; $legacy_params_hash->{_param_key} = undef; } # _unique if (my $id_op = $key_op_hash->{id}) { if ($id_op->{""} or $id_op->{"="}) { $legacy_params_hash->{_unique} = 1; unless ($self->is_unique) { Carp::carp("The BoolExpr includes a filter on ID, but the is_unique flag is unexpectedly false for $self->{id}"); } } } unless ($legacy_params_hash->{_unique}) { if (defined $legacy_params_hash->{id} and not ref $legacy_params_hash->{id}) { # if we have the id, then we have uniqueness # NOT TRUE: we catch the truly unieq cses of having an id and an unambiguous operator above #$legacy_params_hash->{_unique} = 1; } else { # default to non-unique $legacy_params_hash->{_unique} = 0; # if some combination of params can combine to # satisfy at least one unique constraint, # then we have uniqueness in the parameters. my @ps = $subject_class_meta->unique_property_sets; for my $property_set (@ps) { my $property_set = (ref($property_set) ? $property_set : [$property_set]); my @properties_used_from_constraint = grep { defined($_) } (ref($property_set) ? @$key_op_hash{@$property_set} : $key_op_hash->{$property_set}); if (@properties_used_from_constraint == @$property_set) { # filter imprecise operators @properties_used_from_constraint = grep { not ( grep { /^(not |)like(-.|)$/i or /^\[\]/} keys %$_ ) } @properties_used_from_constraint; if (@properties_used_from_constraint == @$property_set) { $legacy_params_hash->{_unique} = 1; last; } else { ## print "some properties use bad operators: @properties_used_from_constraint\n"; } } else { ## print "too few properties in @properties_used_from_constraint\n"; } } } # _param_key gets re-set as long as this has a true value $legacy_params_hash->{_param_key} = undef unless $id_only; } } if ($self->is_unique and not $legacy_params_hash->{_unique}) { Carp::carp "is_unique IS set but legacy params hash is NO for $self->{id}"; #$DB::single = 1; $self->is_unique; } if (!$self->is_unique and $legacy_params_hash->{_unique}) { Carp::carp "is_unique NOT set but legacy params hash IS for $self->{id}"; #$DB::single = 1; $self->is_unique; } $self->{legacy_params_hash} = $legacy_params_hash; return $legacy_params_hash; } sub sorter { my $self = shift; # return a standard sorter for expressions using this template # the template might contain a group_by or order_by clause which affects it... die "this method takes no paramters!" if @_; my $class = $self->subject_class_name; my $sort_meta; if ($self->group_by) { my $set_class = $class . "::Set"; $sort_meta = $set_class->__meta__; } else { $sort_meta = $class->__meta__; } my $sorter; if (my $order_by = $self->order_by) { $sorter = $sort_meta->sorter(@$order_by); } else { $sorter = $sort_meta->sorter(); } return $sorter; } 1; And.pm100664023532023421 10612012544604516 17336 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Templatepackage UR::BoolExpr::Template::And; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::Composite'], ); sub _flatten_bx { my ($class, $bx) = @_; my $template = $bx->template; my ($flattened_template, @extra_values) = $template->_flatten(@_); my $flattened_bx; if (not @extra_values) { # optimized my $flattened_bx_id = $flattened_template->id . $UR::BoolExpr::Util::id_sep . $bx->value_id; $flattened_bx = UR::BoolExpr->get($flattened_bx_id); $flattened_bx->{'values'} = $bx->{'values'} unless $flattened_bx->{'values'}; } else { $flattened_bx = $flattened_template->get_rule_for_values($bx->values, @extra_values); } return $flattened_bx; } sub _reframe_bx { my ($class, $bx, $in_terms_of_property_name) = @_; my $template = $bx->template; my ($reframed_template, @extra_values) = $template->_reframe($in_terms_of_property_name); my $reframed_bx; if (@extra_values == 0) { my $reframed_bx_id = $reframed_template->id . $UR::BoolExpr::Util::id_sep . $bx->value_id; $reframed_bx = UR::BoolExpr->get($reframed_bx_id); $reframed_bx->{'values'} = $bx->{'values'} unless $reframed_bx->{'values'}; } else { my @values = ($bx->values, @extra_values); $reframed_bx = $reframed_template->get_rule_for_values(@values); } return $reframed_bx; } sub _flatten { my $self = $_[0]; if ($self->{flatten}) { return @{ $self->{flatten} } } my @old_keys = @{ $self->_keys }; my $old_property_meta_hash = $self->_property_meta_hash; my $class_meta = $self->subject_class_name->__meta__; my @new_keys; my @extra_keys; my @extra_values; my $old_constant_values; my @new_constant_values; my $found_unflattened_params = 0; while (my $key = shift @old_keys) { my $name = $key; $name =~ s/ .*//; if (! UR::BoolExpr::Util::is_meta_param($name)) { my $mdata = $old_property_meta_hash->{$name}; my ($value_position, $operator) = @$mdata{'value_position','operator'}; my ($flat, $add_keys, $add_values) = $class_meta->_flatten_property_name($name); $found_unflattened_params = 1 if $flat ne $name or @$add_keys or @$add_values; $flat .= ' ' . $operator if $operator and $operator ne '='; push @new_keys, $flat; push @extra_keys, @$add_keys; push @extra_values, @$add_values; } else { push @new_keys, $key; $old_constant_values ||= [ @{ $self->_constant_values } ]; my $old_value = shift @$old_constant_values; my $new_value = []; for my $part (@$old_value) { my ($flat, $add_keys, $add_values) = $class_meta->_flatten_property_name($part); $found_unflattened_params = 1 if $flat ne $name or @$add_keys or @$add_values; push @$new_value, $flat; push @extra_keys, @$add_keys; push @extra_values, @$add_values; } push @new_constant_values, $new_value; } } my $constant_values; if ($old_constant_values) { # some -* keys were found above, and we flattened the value internals $constant_values = \@new_constant_values; } else { # no -* keys, just re-use the empty arrayref $constant_values = $self->_constant_values; } if ($found_unflattened_params or @extra_keys) { if (@extra_keys) { # there may be duplication between these and the primary joins # or each other my %keys_seen = map { $_ => 1 } @new_keys; my @nodup_extra_keys; my @nodup_extra_values; while (my $extra_key = shift @extra_keys) { my $extra_value = shift @extra_values; unless ($keys_seen{$extra_key}) { push @nodup_extra_keys, $extra_key; push @nodup_extra_values, $extra_value; $keys_seen{$extra_key} = 1; } } push @new_keys, @nodup_extra_keys; @extra_values = @nodup_extra_values } my $flat = UR::BoolExpr::Template::And->_fast_construct( $self->subject_class_name, \@new_keys, $constant_values, ); $self->{flatten} = [$flat,@extra_values]; return ($flat, @extra_values); } else { # everything was already flat, just remember this so you DRY $self->{flatten} = [$self]; Scalar::Util::weaken($self->{flatten}[0]); return $self } } sub _reframe { my $self = shift; my $in_terms_of_property_name = shift; # determine the from_class, to_class, and path_back my $from_class = $self->subject_class_name; my $cmeta = $self->subject_class_name->__meta__; my @pmeta = $cmeta->property_meta_for_name($in_terms_of_property_name); unless (@pmeta) { Carp::confess("Failed to find property $in_terms_of_property_name on $from_class. Cannot reframe $self!"); } my @reframe_path_forward = map { $_->_resolve_join_chain($in_terms_of_property_name) } @pmeta; my $to_class = $reframe_path_forward[-1]{foreign_class}; # translate all of the old properties to use the path back to the original class my ($flat,@extra_values) = $self->_flatten; my @old_keys = @{ $flat->_keys }; my $old_property_meta_hash = $flat->_property_meta_hash; my %sub_group_label_used; my $reframer = sub { my $old_name = $_[0]; # uses: @reframe_path_forward from above in this closure # get back to the original object my @reframe_path_back = reverse @reframe_path_forward; # then forward to the property related to it my @filter_path_forward = split('\.',$old_name); # if the end of the path back matches the beginning of the path # to the property in the expression unneeded steps (beyond 1) my $new_key; while (1) { unless (@reframe_path_back) { last; } unless (@filter_path_forward) { last; } my $last_name_back = $reframe_path_back[-1]{source_name_for_foreign}; my $first_name_forward = $filter_path_forward[0]; my $turnaround_match = 0; if ($last_name_back eq $first_name_forward) { # complete overlap $turnaround_match = 1; # safe } else { # see if stripping off any labels makes them match my $last_name_back_base = $last_name_back; $last_name_back_base =~ s/-.*//; my $first_name_forward_base = $first_name_forward; $first_name_forward_base =~ s/-.*//; if ($last_name_back_base eq $first_name_forward_base) { # removing the grouping label causes a match # possible overlap for my $pair ( [$first_name_forward_base, $last_name_back], [$last_name_back_base, $first_name_forward], ) { my ($partial, $full) = @$pair; if (index($full, $partial) == 0) { #print "$partial is part of $full\n"; if (my $prev_full = $sub_group_label_used{$partial}) { # we've tracked back through this $partially specified relationship once # see if we did it the same way if ($prev_full eq $full) { $turnaround_match = 1; } else { #print "previously used $prev_full for $partial: cannot use $full\n"; next; } } else { # this relationship has not been seen #print "using $full for $partial\n"; $sub_group_label_used{$partial} = $full; $turnaround_match = 1; } } } } } if ($turnaround_match == 0) { # found a difference: no shortcut # we have to trek all the way back to the original subject before # moving forward to this property last; } else { # the last step back matches the first step to the property if (@reframe_path_back == 1 and @filter_path_forward == 1) { # just keep one of the identical pair shift @filter_path_forward; } else { # remove both (if one is empty this is no problem) pop @reframe_path_back; shift @filter_path_forward; } } } $new_key = join('.', map { $_->{foreign_name_for_source} } @reframe_path_back); $new_key = join('.', ($new_key ? $new_key : ()), @filter_path_forward); return $new_key; }; # this is only set below if we find any -* keys my $old_constant_values; my @new_keys; my @new_constant_values; while (@old_keys) { my $old_key = shift @old_keys; if (! UR::BoolExpr::Util::is_meta_param($old_key)) { # a regular property my $old_name = $old_key; $old_name =~ s/ .*//; my $mdata = $old_property_meta_hash->{$old_name}; my ($value_position, $operator) = @$mdata{'value_position','operator'}; my $new_key = $reframer->($old_name); $new_key .= ' ' . $operator if $operator and $operator ne '='; push @new_keys, $new_key; } else { # this key is not a property, it's a special key like -order_by or -group_by unless ($old_key eq '-order_by' or $old_key eq '-group_by' or $old_key eq '-hints' or $old_key eq '-recurse' ) { Carp::confess("no support yet for $old_key in bx reframe()!"); } push @new_keys, $old_key; unless ($old_constant_values) { $old_constant_values = [ @{ $flat->_constant_values } ]; } my $old_value = shift @$old_constant_values; my $new_value = []; for my $part (@$old_value) { my $reframed_part = $reframer->($part); push @$new_value, $reframed_part; } push @new_constant_values, $new_value; } } my $constant_values; if (@new_constant_values) { $constant_values = \@new_constant_values; } else { $constant_values = $flat->_constant_values; # re-use empty immutable arrayref } my $reframed = UR::BoolExpr::Template::And->_fast_construct( $to_class, \@new_keys, $constant_values, ); return $reframed, @extra_values; } sub _template_for_grouped_subsets { my $self = shift; my $group_by = $self->group_by; die "rule template $self->{id} has no -group_by!?!?" unless $group_by; my @base_property_names = $self->_property_names; for (my $i = 0; $i < @base_property_names; $i++) { my $operator = $self->operator_for($base_property_names[$i]); if ($operator ne '=') { $base_property_names[$i] .= " $operator"; } } my $template = UR::BoolExpr::Template->get_by_subject_class_name_logic_type_and_logic_detail( $self->subject_class_name, 'And', join(",", @base_property_names, @$group_by), ); return $template; } sub _variable_value_count { my $self = shift; my $k = $self->_underlying_keys; my $v = $self->_constant_values; if ($v) { $v = scalar(@$v); } else { $v = 0; } return $k-$v; } sub _underlying_keys { my $self = shift; my $logic_detail = $self->logic_detail; return unless $logic_detail; my @underlying_keys = split(",",$logic_detail); return @underlying_keys; } sub get_underlying_rule_templates { my $self = shift; my @underlying_keys = grep { UR::BoolExpr::Util::is_meta_param($_) ? () : ($_) } $self->_underlying_keys(); my $subject_class_name = $self->subject_class_name; return map { UR::BoolExpr::Template::PropertyComparison ->_get_for_subject_class_name_and_logic_detail( $subject_class_name, $_ ); } @underlying_keys; } sub specifies_value_for { my ($self, $property_name) = @_; Carp::confess('Missing required parameter property_name for specifies_value_for()') if not defined $property_name; my @underlying_templates = $self->get_underlying_rule_templates(); foreach ( @underlying_templates ) { return 1 if $property_name eq $_->property_name; } return; } sub _filter_breakdown { my $self = $_[0]; my $filter_breakdown = $self->{_filter_breakdown} ||= do { my @underlying = $self->get_underlying_rule_templates; my @primary; my %sub_group_filters; my %sub_group_sub_filters; for (my $n = 0; $n < @underlying; $n++) { my $underlying = $underlying[$n]; my $sub_group = $underlying->sub_group; if ($sub_group) { if (substr($sub_group,-1) ne '?') { # control restruct the subject based on the sub-group properties my $list = $sub_group_filters{$sub_group} ||= []; push @$list, $underlying, $n; } else { # control what is IN a sub-group (effectively define it with these) chop($sub_group); my $list = $sub_group_sub_filters{$sub_group} ||= []; push @$list, $underlying, $n; } } else { push @primary, $underlying, $n; } } { primary => \@primary, sub_group_filters => \%sub_group_filters, sub_group_sub_filters => \%sub_group_sub_filters, }; }; return $filter_breakdown; } sub evaluate_subject_and_values { my $self = shift; my $subject = shift; return unless (ref($subject) && $subject->isa($self->subject_class_name)); my $filter_breakdown = $self->_filter_breakdown; my ($primary,$sub_group_filters,$sub_group_sub_filters) = @$filter_breakdown{"primary","sub_group_filters","sub_group_sub_filters"}; # flattening expresions now requires that we re-group them :( # these effectively are subqueries where they occur # check the ungrouped comparisons first since they are simpler for (my $n = 0; $n < @$primary; $n+=2) { my $underlying = $primary->[$n]; my $pos = $primary->[$n+1]; my $value = $_[$pos]; unless ($underlying->evaluate_subject_and_values($subject, $value)) { return; } } # only check the complicated rules if none of the above failed if (%$sub_group_filters) { #$DB::single = 1; for my $sub_group (keys %$sub_group_filters) { my $filters = $sub_group_filters->{$sub_group}; my $sub_filters = $sub_group_sub_filters->{$sub_group}; print "FILTERING $sub_group: " . Data::Dumper::Dumper($filters, $sub_filters); } } return 1; } sub params_list_for_values { # This is the reverse of the bulk of resolve. # It returns the params in list form, directly coercable into a hash if necessary. # $r = UR::BoolExpr->resolve($c1,@p1); # ($c2, @p2) = ($r->subject_class_name, $r->params_list); my $rule_template = shift; my @values_sorted = @_; my @keys_sorted = $rule_template->_underlying_keys; my $constant_values = $rule_template->_constant_values; my @params; my ($v,$c) = (0,0); for (my $k=0; $k<@keys_sorted; $k++) { my $key = $keys_sorted[$k]; #if (substr($key,0,1) eq "_") { # next; #} if (UR::BoolExpr::Util::is_meta_param($key)) { my $value = $constant_values->[$c]; push @params, $key, $value; $c++; } else { my ($property, $op) = ($key =~ /^(\-*[\w\.]+)\s*(.*)$/); unless ($property) { $DB::single = 1; Carp::confess("bad key '$key' in ",join(', ', @keys_sorted)); } my $value = $values_sorted[$v]; if ($op) { if ($op ne "in") { if ($op =~ /^(.+)-(.+)$/) { $value = { operator => $1, value => $value, escape => $2 }; } else { $value = { operator => $op, value => $value }; } } } push @params, $property, $value; $v++; } } return @params; } sub _fast_construct { my ($class, $subject_class_name, # produces subject class meta $keys, # produces logic detail $constant_values, # produces constant value id $logic_detail, # optional, passed by get $constant_value_id, # optional, passed by get $subject_class_meta, # optional, passed by bx ) = @_; my $logic_type = 'And'; $logic_detail ||= join(",",@$keys); $constant_value_id ||= UR::BoolExpr::Util::values_to_value_id(@$constant_values); my $id = join('/',$subject_class_name,$logic_type,$logic_detail,$constant_value_id); my $self = $UR::Object::rule_templates->{$id}; return $self if $self; $subject_class_meta ||= $subject_class_name->__meta__; # See what properties are id-related for the class my $cache = $subject_class_meta->{cache}{'UR::BoolExpr::Template::get'} ||= do { my $id_related = {}; my $id_translations = []; my $id_pos = {}; my $id_prop_is_real; # true if there's a property called 'id' that's a real property, not from UR::Object for my $iclass ($subject_class_name, $subject_class_meta->ancestry_class_names) { last if $iclass eq "UR::Object"; next unless $iclass->isa("UR::Object"); my $iclass_meta = $iclass->__meta__; my @id_props = $iclass_meta->id_property_names; next unless @id_props; $id_prop_is_real = 1 if (grep { $_ eq 'id'} @id_props); next if @id_props == 1 and $id_props[0] eq "id" and !$id_prop_is_real; @$id_related{@id_props} = @id_props; push @$id_translations, \@id_props; @$id_pos{@id_props} = (0..$#id_props) unless @id_props == 1 and $id_props[0] eq 'id'; } [$id_related,$id_translations,$id_pos]; }; my ($id_related,$id_translations,$id_pos) = @$cache; my @keys = @$keys; my @constant_values = @$constant_values; # Make a hash to quick-validate the params for duplication no warnings; my %check_for_duplicate_rules; for (my $n=0; $n < @keys; $n++) { next if UR::BoolExpr::Util::is_meta_param($keys[$n]); my $pos = index($keys[$n],' '); if ($pos != -1) { my $property = substr($keys[$n],0,$pos); $check_for_duplicate_rules{$property}++; } else { $check_for_duplicate_rules{$keys[$n]}++; } } # each item in this list mutates the initial set of key-value pairs my $extenders = []; # add new @$extenders for class-specific characteristics # add new @keys at the same time # flag keys as removed also at the same time # note the positions for each key in the "original" rule # by original, we mean the original plus the extensions from above # my $id_position = undef; my $var_pos = 0; my $const_pos = 0; my $property_meta_hash = {}; my $property_names = []; for my $key (@keys) { if (UR::BoolExpr::Util::is_meta_param($key)) { $property_meta_hash->{$key} = { name => $key, value_position => $const_pos }; $const_pos++; } else { my ($name, $op) = ($key =~ /^(.+?)\s+(.*)$/); $name ||= $key; if ($name eq 'id') { $id_position = $var_pos; } $property_meta_hash->{$name} = { name => $name, operator => $op, value_position => $var_pos }; $var_pos++; push @$property_names, $name; } } # Note whether there are properties not involved in the ID # Add value extenders for any cases of id-related properties, # or aliases. my $original_key_count = @keys; my $id_only = 1; my $partial_id = 0; my $key_op_hash = {}; if (@$id_translations and @{$id_translations->[0]} == 1) { # single-property ID ## use Data::Dumper; ## print "single property id\n". Dumper($id_translations); my ($property, $op); # Presume we are only getting id properties until another is found. # If a multi-property is partially specified, we'll zero this out too. my $values_index = -1; # -1 so we can bump it at start of loop for (my $key_pos = 0; $key_pos < $original_key_count; $key_pos++) { my $key = $keys[$key_pos]; if (UR::BoolExpr::Util::is_meta_param($key)) { # -* are constant value keys and do not need to be changed next; } else { $values_index++; } my ($property, $op) = ($key =~ /^(.+?)\s+(.*)$/); $property ||= $key; $op ||= ""; $op =~ s/\s+//; $key_op_hash->{$property} ||= {}; $key_op_hash->{$property}{$op}++; if ($property eq "id" or $id_related->{$property}) { # Put an id key into the key list. for my $alias (["id"], @$id_translations) { next if $alias->[0] eq $property; next if $check_for_duplicate_rules{$alias->[0]}; $op ||= ""; push @keys, $alias->[0] . ($op ? " $op" : ""); push @$extenders, [ [$values_index], undef, $keys[-1] ]; $key_op_hash->{$alias->[0]} ||= {}; $key_op_hash->{$alias->[0]}{$op}++; ## print ">> extend for @$alias with op $op.\n"; } unless ($op =~ m/^(=|eq|in|\[\]|)$/) { $id_only = 0; } } elsif (! UR::BoolExpr::Util::is_meta_param($key)) { $id_only = 0; ## print "non id single property $property on $subject_class\n"; } } } else { # multi-property ID ## print "multi property id\n". Dumper($id_translations); my ($property, $op); my %id_parts; my $values_index = -1; # -1 so we can bump it at start of loop my $id_op; for (my $key_pos = 0; $key_pos < $original_key_count; $key_pos++) { my $key = $keys[$key_pos]; if (UR::BoolExpr::Util::is_meta_param($key)) { # -* are constant value keys and do not need to be changed next; } else { $values_index++; } next if UR::BoolExpr::Util::is_meta_param($key); my ($property, $op) = ($key =~ /^(.+?)\s+(.*)$/); $property ||= $key; $op ||= ''; $op =~ s/^\s+// if $op; $key_op_hash->{$property} ||= {}; $key_op_hash->{$property}{$op}++; if ($property eq "id") { $id_op = $op; $key_op_hash->{id} ||= {}; $key_op_hash->{id}{$op}++; # Put an id-breakdown key into the key list. for my $alias (@$id_translations) { my @new_keys = map { $_ . ($op ? " $op" : "") } @$alias; if (grep { $check_for_duplicate_rules{$_} } @$alias) { #print "up @new_keys with @$alias\n"; } else { push @keys, @new_keys; push @$extenders, [ [$values_index], "resolve_ordered_values_from_composite_id", @new_keys ]; for (@$alias) { $key_op_hash->{$_} ||= {}; $key_op_hash->{$_}{$op}++; } # print ">> extend for @$alias with op $op.\n"; } } } elsif ($id_related->{$property}) { $id_op ||= $op; if ($op eq "" or $op eq "eq" or $op eq "=" or $op eq 'in') { $id_parts{$id_pos->{$property}} = $values_index; } else { # We're doing some sort of gray-area comparison on an ID # field, and though we could possibly resolve an ID # from things like an 'in' op, it's more than we've done # before. $id_only = 0; } } else { ## print "non id multi property $property on class $subject_class\n"; $id_only = 0; } } if (my $parts = (scalar(keys(%id_parts)))) { # some parts are id-related if ($parts == @{$id_translations->[0]}) { # all parts are of the id are there if (@$id_translations) { if (grep { $_ eq 'id' } @keys) { #print "found id already\n"; } else { #print "no id\n"; # we have translations of that ID into underlying properties #print "ADDING ID for " . join(",",keys %id_parts) . "\n"; my @id_pos = sort { $a <=> $b } keys %id_parts; push @$extenders, [ [@id_parts{@id_pos}], "resolve_composite_id_from_ordered_values", 'id' ]; #TODO was this correct? $key_op_hash->{id} ||= {}; $key_op_hash->{id}{$id_op}++; push @keys, "id"; } } } else { # not all parts of the id are there ## print "partial id property $property on class $subject_class\n"; $id_only = 0; $partial_id = 1; } } else { $id_only = 0; $partial_id = 0; } } # Determine the positions of each key in the parameter list. # In actuality, the position of the key's value in the @values or @constant_values array, # depending on whether it is a -* key or not. my %key_positions; my $vpos = 0; my $cpos = 0; for my $key (@keys) { $key_positions{$key} ||= []; if (UR::BoolExpr::Util::is_meta_param($key)) { push @{ $key_positions{$key} }, $cpos++; } else { push @{ $key_positions{$key} }, $vpos++; } } # Sort the keys, and make an arrayref which will # re-order the values to match. my $last_key = ''; my @keys_sorted = map { $_ eq $last_key ? () : ($last_key = $_) } sort @keys; my $normalized_positions_arrayref = []; my $constant_value_normalized_positions = []; my $recursion_desc = undef; my $hints = undef; my $order_by = undef; my $group_by = undef; my $page = undef; my $limit = undef; my $offset = undef; my $aggregate = undef; my @constant_values_sorted; for my $key (@keys_sorted) { my $pos_list = $key_positions{$key}; my $pos = pop @$pos_list; if (UR::BoolExpr::Util::is_meta_param($key)) { push @$constant_value_normalized_positions, $pos; my $constant_value = $constant_values[$pos]; if ($key eq '-recurse') { $constant_value = [$constant_value] if (!ref $constant_value); $recursion_desc = $constant_value; } elsif ($key eq '-hints' or $key eq '-hint') { $constant_value = [$constant_value] if (!ref $constant_value); $hints = $constant_value; } elsif ($key eq '-order_by' or $key eq '-order') { $constant_value = [$constant_value] if (!ref $constant_value); $order_by = $constant_value; } elsif ($key eq '-group_by' or $key eq '-group') { $constant_value = [$constant_value] if (!ref $constant_value); $group_by = $constant_value; } elsif ($key eq '-page') { $constant_value = [$constant_value] if (!ref $constant_value); $page = $constant_value; } elsif ($key eq '-limit') { $limit = $constant_value; } elsif ($key eq '-offset') { $offset = $constant_value; } elsif ($key eq '-aggregate') { $constant_value = [$constant_value] if (!ref $constant_value); $aggregate = $constant_value; } else { Carp::croak("Unknown special param '$key'. Expected one of: @UR::BoolExpr::Template::meta_param_names"); } push @constant_values_sorted, $constant_value; } else { push @$normalized_positions_arrayref, $pos; } } if ($page) { if (defined($limit) || defined($offset)) { Carp::croak("-page and -limit/-offset are mutually exclusive when defining a BoolExpr"); } if (ref($page) and ref($page) eq 'ARRAY') { if (@$page == 2) { $limit = $page->[1]; $offset = ($page->[0] - 1) * $limit; } elsif (@$page) { Carp::croak('-page must be an arrayref of two integers: -page => [$page_number, $page_size]'); } } else { Carp::croak('-page must be an arrayref of two integers: -page => [$page_number, $page_size]'); } } if (defined($hints) and ref($hints) ne 'ARRAY') { if (! ref($hints)) { $hints = [$hints]; # convert it to a list of one item } else { Carp::croak('-hints of a rule must be an arrayref of property names'); } } my $matches_all = scalar(@keys_sorted) == scalar(@constant_values); $id_only = 0 if ($matches_all); # these are used to rapidly turn a bx used for querying into one # suitable for object construction my @ambiguous_keys; my @ambiguous_property_names; for (my $n=0; $n < @keys; $n++) { next if UR::BoolExpr::Util::is_meta_param($keys[$n]); my ($property, $op) = ($keys[$n] =~ /^(.+?)\s+(.*)$/); $property ||= $keys[$n]; $op ||= ''; $op =~ s/^\s+// if $op; if ($op and $op ne 'eq' and $op ne '==' and $op ne '=') { push @ambiguous_keys, $keys[$n]; push @ambiguous_property_names, $property; } } # Determine the rule template's ID. # The normalizer will store this. Below, we'll # find or create the template for this ID. my $normalized_constant_value_id = (scalar(@constant_values_sorted) ? UR::BoolExpr::Util::values_to_value_id(@constant_values_sorted) : $constant_value_id); my @keys_unaliased = $UR::Object::Type::bootstrapping ? @keys_sorted : map { $_->[0] = UR::BoolExpr::Util::is_meta_param($_->[0]) ? $_->[0] : $subject_class_meta->resolve_property_aliases($_->[0]); join(' ',@$_); } map { [ split(' ') ] } @keys_sorted; my $normalized_id = UR::BoolExpr::Template->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, "And", join(",",@keys_unaliased), $normalized_constant_value_id); $self = bless { id => $id, subject_class_name => $subject_class_name, logic_type => $logic_type, logic_detail => $logic_detail, constant_value_id => $constant_value_id, normalized_id => $normalized_id, # subclass specific id_position => $id_position, is_id_only => $id_only, is_partial_id => $partial_id, is_unique => undef, # assigned on first use matches_all => $matches_all, key_op_hash => $key_op_hash, _property_names_arrayref => $property_names, _property_meta_hash => $property_meta_hash, recursion_desc => $recursion_desc, hints => $hints, order_by => $order_by, group_by => $group_by, limit => $limit, offset => $offset, aggregate => $aggregate, is_normalized => ($id eq $normalized_id ? 1 : 0), normalized_positions_arrayref => $normalized_positions_arrayref, constant_value_normalized_positions_arrayref => $constant_value_normalized_positions, normalization_extender_arrayref => $extenders, num_values => scalar(@$keys), _keys => \@keys, _constant_values => $constant_values, _ambiguous_keys => (@ambiguous_keys ? \@ambiguous_keys : undef), _ambiguous_property_names => (@ambiguous_property_names ? \@ambiguous_property_names : undef), }, 'UR::BoolExpr::Template::And'; $UR::Object::rule_templates->{$id} = $self; return $self; } 1; =pod =head1 NAME UR::BoolExpr::And - a rule which is true if ALL the underlying conditions are true =head1 SEE ALSO UR::BoolExpr;(3) =cut Composite.pm100664023532023421 303712544604516 20541 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Templatepackage UR::BoolExpr::Template::Composite; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template'], ); # sub _underlying_keys { sub get_underlying_rules_for_values { my $self = shift; my @values = @_; my @underlying_templates = $self->get_underlying_rule_templates(); my @underlying_rules; for my $template (@underlying_templates) { my $n = $template->_variable_value_count; my $rule = $template->get_rule_for_values(splice(@values,0,$n)); push @underlying_rules, $rule; } return @underlying_rules; } sub _get_for_subject_class_name_and_logic_detail { my $class = shift; my $subject_class_name = shift; my $logic_detail = shift; my $constant_id = shift; my ($logic_type) = ($class =~ /^UR::BoolExpr::Template::(.*)/); my $id = $class->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, $logic_type, $logic_detail, $constant_id); return $class->get($id); } # sub get_underlying_rule_templates { # sub specifies_value_for { # evalutate_subject_and_values { 1; =pod =head1 NAME UR::BoolExpr::Composite - an "and" or "or" rule =head1 SYNOPSIS @r = $r->get_underlying_rules(); for (@r) { print $r->evaluate($c1); } =head1 DESCRIPTION =head1 SEE ALSO UR::Object(3), UR::BoolExpr, UR::BoolExpr::Template, UR::BoolExpr::Template::And, UR::BoolExpr::Template::Or, UR::BoolExpr::Template::PropertyComparison =cut Or.pm100664023532023421 1313212544604516 17174 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Templatepackage UR::BoolExpr::Template::Or; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::Composite'], ); sub _flatten_bx { my ($class, $bx) = @_; my @old = $bx->underlying_rules; my @new; for my $old (@old) { my $new = $old->flatten; push @new, [ $new->_params_list ]; } my $flattened_bx = $class->_compose($bx->subject_class_name,\@new); return $flattened_bx; } sub _reframe_bx { my ($class, $bx, $in_terms_of) = @_; my @old = $bx->underlying_rules; my @new; for my $old (@old) { my $new = $old->reframe($in_terms_of); push @new, [ $new->_params_list ]; } my @meta = $bx->subject_class_name->__meta__->property_meta_for_name($in_terms_of); my @joins = $meta[-1]->_resolve_join_chain($in_terms_of); my $reframed_bx = $class->_compose($joins[-1]{foreign_class},\@new); return $reframed_bx; } sub _compose { my $self = shift; my $subject_class = shift; my $sub_queries = shift; my $meta_params = shift; my @underlying_rules; my @expressions; my @values; while (@$sub_queries) { my $underlying_query; if (ref($sub_queries->[0]) eq 'ARRAY') { $underlying_query = UR::BoolExpr->resolve($subject_class, @{$sub_queries->[0]}, @$meta_params); shift @$sub_queries; } elsif (ref($sub_queries->[0]) eq 'UR::BoolExpr::And') { $underlying_query = shift @$sub_queries; } else { $underlying_query = UR::BoolExpr->resolve($subject_class, @$sub_queries[0,1], @$meta_params); shift @$sub_queries; shift @$sub_queries; } if ($underlying_query->{'_constant_values'}) { Carp::confess("cannot use -* expressions in subordinate clauses of a logical "); } unless ($underlying_query->template->isa("UR::BoolExpr::Template::And")) { Carp::confess("$underlying_query is not an AND template"); } push @underlying_rules, $underlying_query; push @expressions, $underlying_query->template->logic_detail; push @values, $underlying_query->values; } my $bxt = UR::BoolExpr::Template::Or->get_by_subject_class_name_logic_type_and_logic_detail($subject_class,'Or',join('|',@expressions)); my $bx = $bxt->get_rule_for_values(@values); # This (and accompanying "caching" in UR::BoolExpr::underlying_rules()) # is a giant hack to allow composite rules to have -order and -group # The real fix is to coax the above combination of # get_by_subject_class_name_logic_type_and_logic_detail() and get_rule_for_values() to # properly encode these constant/template values into the rule and template IDs, # and subsequently reconsitiute them when you call $template->order_by $bx->{'_underlying_rules'} = \@underlying_rules; for (my $i = 0; $i < @$meta_params; $i += 2) { my $method = $meta_params->[$i]; substr($method, 0, 1, ''); # remove the - if ($method eq 'recurse') { $bx->template->recursion_desc($meta_params->[$i + 1]); } elsif ($method eq 'order') { $bx->template->order_by($meta_params->[$i + 1]); } else { $bx->template->$method($meta_params->[$i + 1]); } } return $bx; } sub _underlying_keys { my $self = shift; my $logic_detail = $self->logic_detail; return unless $logic_detail; my @underlying_keys = split('\|',$logic_detail); return @underlying_keys; } # sub get_underlying_rules_for_values sub get_underlying_rule_templates { my $self = shift; my @underlying_keys = $self->_underlying_keys(); my $subject_class_name = $self->subject_class_name; return map { UR::BoolExpr::Template::And ->_get_for_subject_class_name_and_logic_detail( $subject_class_name, $_ ); } @underlying_keys; } sub specifies_value_for { my ($self, $property_name) = @_; Carp::confess() if not defined $property_name; my @underlying_templates = $self->get_underlying_rule_templates(); my @all_specified; for my $template (@underlying_templates) { my @specified = $template->specifies_value_for($property_name); if (@specified) { push @all_specified, @specified; } else { return; } } return @all_specified; } sub evaluate_subject_and_values { my $self = shift; my $subject = shift; return unless (ref($subject) && $subject->isa($self->subject_class_name)); my @underlying = $self->get_underlying_rule_templates; while (my $underlying = shift (@underlying)) { my $n = $underlying->_variable_value_count; my @next_values = splice(@_,0,$n); if ($underlying->evaluate_subject_and_values($subject,@_)) { return 1; } } return; } sub params_list_for_values { my $self = shift; my @values_sorted = @_; my @list; my @t = $self->get_underlying_rule_templates; for my $t (@t) { my $c = $t->_variable_value_count; my @l = $t->params_list_for_values(splice(@values_sorted,0,$c)); push @list, \@l; } return -or => \@list; } sub get_normalized_rule_for_values { my $self = shift; return $self->get_rule_for_values(@_); } 1; =pod =head1 NAME UR::BoolExpr::Or - a rule which is true if ANY of the underlying conditions are true =head1 SEE ALSO UR::BoolExpr;(3) =cut PropertyComparison.pm100664023532023421 734412544604516 22463 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template package UR::BoolExpr::Template::PropertyComparison; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; # Define the class metadata. require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template'], #has => [qw/ # rule_type # subject_class_name # property_name # comparison_operator # value # resolution_code_perl # resolution_code_sql #/], #id_by => ['subject_class_name','logic_string'] ); use UR::BoolExpr::Template::PropertyComparison::Equals; use UR::BoolExpr::Template::PropertyComparison::LessThan; use UR::BoolExpr::Template::PropertyComparison::In; use UR::BoolExpr::Template::PropertyComparison::Like; sub property_name { (split(' ',$_[0]->logic_detail))[0] } sub comparison_operator { (split(' ',$_[0]->logic_detail))[1] } sub sub_group { my $self = shift; my $spec = $self->property_name; if ($spec =~ /-/) { #$DB::single = 1; } if ($spec =~ /^(.*)+\-(\w+)(\?|)(\..+|)/) { return $2 . $3; } else { return ''; } } sub get_underlying_rules_for_values { return; } sub num_values { # Not strictly correct... return 1; } sub evaluate_subject_and_values { my ($self,$subject,$comparison_value) = @_; my @property_values = $subject->__get_attr__($self->property_name); return $self->_compare($comparison_value, @property_values); } sub resolve_subclass_for_comparison_operator { my $class = shift; my $comparison_operator = shift; # Remove any escape sequence that may have been put in at UR::BoolExpr::resolve() $comparison_operator =~ s/-.+$// if $comparison_operator; my $suffix = UR::Util::class_suffix_for_operator($comparison_operator); my $subclass_name = join('::', $class, $suffix); my $subclass_meta = UR::Object::Type->get($subclass_name); unless ($subclass_meta) { Carp::confess("Unknown operator '$comparison_operator'"); } return $subclass_name; } sub _get_for_subject_class_name_and_logic_detail { my $class = shift; my $subject_class_name = shift; my $logic_detail = shift; my ($property_name, $comparison_operator) = split(' ',$logic_detail, 2); my $subclass_name = $class->resolve_subclass_for_comparison_operator($comparison_operator); my $id = $subclass_name->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, 'PropertyComparison', $logic_detail); return $subclass_name->get($id); } sub comparison_value_and_escape_character_to_regex { my ($class, $value, $escape) = @_; return '' unless defined($value); # anyone who uses the % as an escape character deserves to suffer if ($value eq '%') { return '^.+$'; } my $regex = $value; # Escape all special characters in the regex. $regex =~ s/([\(\)\[\]\{\}\+\*\.\?\|\^\$\-])/\\$1/g; # Handle the escape sequence if (defined $escape) { $escape =~ s/\\/\\\\/g; # replace \ with \\ $regex =~ s/(?define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($self, $value, @property_value) = @_; my $lower_bound = $value->[0]; my $upper_bound = $value->[1]; my $cv_is_number = Scalar::Util::looks_like_number($lower_bound) and Scalar::Util::looks_like_number($upper_bound); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value >= $lower_bound and $property_value <= $upper_bound); } else { return 1 if ( $property_value ge $lower_bound and $property_value le $upper_bound); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Between - perform a 'between' test =head1 DESCRIPTION Evaluates to true of the property's value is between the lower and upper bounds, inclusive. If the property returns multiple values, this comparison returns true if any of the values are within the bounds. =cut Equals.pm100664023532023421 222412544604516 23705 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::Equals; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_values) = @_; no warnings 'uninitialized'; if (@property_values == 0) { return ($comparison_value eq '' ? 1 : ''); } no warnings 'numeric'; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); foreach my $property_value ( @property_values ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($pv_is_number and $cv_is_number) { return 1 if $property_value == $comparison_value; } else { return 1 if $property_value eq $comparison_value; } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Equals - perform a strictly equals test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are equal to the comparison value =cut False.pm100664023532023421 143212544604516 23505 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::False; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings; if (@property_value == 0) { return 1; } else { for (@property_value) { return 1 if (! $_); # Returns true if _any_ of the values are false } return ''; } } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::False - evaluates to true if the property's value is false If the property returns multiple values, this comparison returns true if any of the values are false =cut GreaterOrEqual.pm100664023532023421 211412544604516 25333 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings qw(numeric uninitialized); foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value >= $comparison_value ); } else { return 1 if ( $property_value ge $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual - perform a greater than or equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are greater or equal to the comparison value =cut GreaterThan.pm100664023532023421 206612544604516 24663 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::GreaterThan; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value > $comparison_value ); } else { return 1 if ( $property_value gt $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::GreaterThan - perform a greater than test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are greater than the comparison value =cut In.pm100664023532023421 335012544604516 23022 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::In; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], doc => "Returns true if any of the property's values appears in the comparison value list", ); sub _compare { my ($class,$comparison_values,@property_values) = @_; if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # undef should match missing values, which will be sorted at the end - the sorter in # UR::BoolExpr::resolve() takes care of the sorting for us if (! @property_values and !defined($comparison_values->[-1])) { return 1; } my($pv_idx, $cv_idx); no warnings; my $sorter = sub { return $property_values[$pv_idx] cmp $comparison_values->[$cv_idx] }; use warnings; # Binary search within @$comparison_values for ( $pv_idx = 0; $pv_idx < @property_values; $pv_idx++ ) { my $cv_min = 0; my $cv_max = $#$comparison_values; do { $cv_idx = ($cv_min + $cv_max) >> 1; my $result = &$sorter; if (!$result) { return 1; } elsif ($result > 0) { $cv_min = $cv_idx + 1; } else { $cv_max = $cv_idx - 1; } } until ($cv_min > $cv_max); } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::In - perform an In test =head1 DESCRIPTION Returns true if any of the property's values appears in the comparison value list. Think of 'in' as short for 'intersect', and not just SQL's 'IN' operator. =cut Isa.pm100664023532023421 224012544604516 23165 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::Isa; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_values) = @_; if (ref $comparison_value) { # Reference... maybe an Object? if (eval { $comparison_value->isa('UR::Object::Type')} ) { # It's a class object. Compare to the Class's class_name $comparison_value = $comparison_value->class_name; } else { # It's an object... test on that object's type $comparison_value = ref($comparison_value); } } foreach my $property_value ( @property_values ) { return 1 if (eval { $property_value->isa($comparison_value) }); } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Isa - Test whether a value is-a subclass of another class =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are a subclass of the comparison value =cut LessOrEqual.pm100664023532023421 207512544604516 24656 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::LessOrEqual; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value <= $comparison_value ); } else { return 1 if ( $property_value le $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::LessOrEqual - perform a less than or equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are less or equal to the comparison value =cut LessThan.pm100664023532023421 204612544604516 24176 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::LessThan; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value < $comparison_value ); } else { return 1 if ( $property_value lt $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::LessThan - perform a less than test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are less than the comparison value =cut Like.pm100664023532023421 227512544604516 23345 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::Like; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; return '' unless defined ($comparison_value); # property like NULL should always be false my $escape = '\\'; my $regex = $class-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); no warnings 'uninitialized'; foreach my $value ( @property_value ) { return 1 if $value =~ $regex; } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Like - perform an SQL-ish like test =head1 DESCRIPTION The input test value is assummed to be an SQL 'like' value, where '_' represents a one character wildcard, and '%' means a 0 or more character wildcard. It gets converted to a perl regular expression and used to match against an object's properties. If the property returns multiple values, this comparison returns true if any of the values match. =cut Matches.pm100664023532023421 135212544604516 24040 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::Matches; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { return 1 if ( $property_value =~ m/$comparison_value/ ); } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Matches - perform a Perl regular expression match =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values match =cut NotBetween.pm100664023532023421 251412544604516 24527 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::NotBetween; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($self, $value, @property_value) = @_; my $lower_bound = $value->[0]; my $upper_bound = $value->[1]; my $cv_is_number = Scalar::Util::looks_like_number($lower_bound) and Scalar::Util::looks_like_number($upper_bound); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value < $lower_bound or $property_value > $upper_bound); } else { return 1 if ( $property_value lt $lower_bound or $property_value gt $upper_bound); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotBetween - perform a 'not between' test =head1 DESCRIPTION Evaluates to true of the property's value is not between the lower and upper bounds, inclusive. If the property returns multiple values, this comparison returns true if any of the values are outside the bounds. =cut NotEquals.pm100664023532023421 217712544604516 24375 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotEquals; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings 'uninitialized'; if (@property_value == 0) { return ($comparison_value eq '' ? '' : 1); } my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return '' if ( $property_value == $comparison_value ); } else { return '' if ( $property_value eq $comparison_value ); } } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotEqual - perform a not-equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns false if any if the values are equal to the comparison value =cut NotIn.pm100664023532023421 325412544604516 23506 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotIn; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], doc => "Returns false if any of the property's values appears in the comparison value list", ); sub _compare { my ($class,$comparison_values,@property_values) = @_; if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # undef should match missing values, which will be sorted at the end - the sorter in # UR::BoolExpr::resolve() takes care of the sorting for us if (! @property_values and !defined($comparison_values->[-1])) { return ''; } my($pv_idx, $cv_idx); no warnings; my $sorter = sub { return $property_values[$pv_idx] cmp $comparison_values->[$cv_idx] }; use warnings; # Binary search within @$comparison_values my $cv_min = 0; my $cv_max = $#$comparison_values; for ( $pv_idx = 0; $pv_idx < @property_values; $pv_idx++ ) { do { $cv_idx = ($cv_min + $cv_max) >> 1; my $result = &$sorter; if (!$result) { return ''; } elsif ($result > 0) { $cv_min = $cv_idx + 1; } else { $cv_max = $cv_idx - 1; } } until ($cv_min > $cv_max); } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotIn - perform a negated In comparison =head1 DESCRIPTION Returns false if any of the property's values appears in the comparison value list =cut NotLike.pm100664023532023421 222312544604516 24017 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotLike; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $escape = '\\'; my $regex = $class-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { return '' if ($property_value =~ $regex); } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotLike - perform a negated SQL-ish like test =head1 DESCRIPTION The input test value is assummed to be an SQL 'like' value, where '_' represents a one character wildcard, and '%' means a 0 or more character wildcard. It gets converted to a perl regular expression and used in a negated match against an object's properties If the property returns multiple values, this comparison returns false if any of the values matches the pattern =cut True.pm100664023532023421 142612544604516 23375 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::True; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings; if (@property_value == 0) { return ''; } else { for (@property_value) { return 1 if ($_); # Returns true if _any_ of the values are true } return ''; } } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::True - Evaluates to true if the property's value is true If the property returns multiple values, this comparison returns true if any of the values are true =cut Util.pm100664023532023421 1455512544604516 15770 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/BoolExpr package UR::BoolExpr::Util; # Non-OO Utility methods for the rule modules. use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use Scalar::Util qw(blessed reftype refaddr); use Data::Dumper; use FreezeThaw; # Because the id is actually a full data structure we need some separators. # Note that these are used for the common case, where FreezeThaw is for arbitrarily complicated rule identifiers. our $id_sep = chr(29); # spearetes id property values instead of the old \t our $record_sep = chr(30); # within a value_id, delimits a distinct values our $unit_sep = chr(31); # seperates items within a single value our $null_value = chr(21); # used for undef/null our $empty_string = chr(28); # used for "" our $empty_list = chr(20); # used for [] # These are used when there is any sort of complicated data in the rule. sub values_to_value_id_frozen { my $frozen = FreezeThaw::safeFreeze(@_); return "F:" . $frozen; } sub value_id_to_values_frozen { my $value_id = shift; return _fixup_ur_objects_from_thawed_data(FreezeThaw::thaw($value_id)); } sub _fixup_ur_objects_from_thawed_data { my @values = @_; our $seen; local $seen = $seen; $seen ||= {}; # For things that are UR::Objects (or used to be UR objects), swap the # thawed/cloned one with one from the object cache # # This sub is localized inside _fixup_ur_objects_from_thawed_data so it's not called # externally, and uses $_ as the thing to process, which is set in the foreach loop # below - both as a performance speedup of# not having to prepare an argument list while # processing a possibly deep data structure, and clarity of avoiding double dereferencing # as this sub needs to mutate the item it's processing my $process_it = sub { if (blessed($_) and ( $_->isa('UR::Object') or $_->isa('UR::BoolExpr::Util::clonedThing') ) ) { my($class, $id) = ($_->class, $_->id); if (refaddr($_) != refaddr($UR::Context::all_objects_loaded->{$class}->{$id})) { # bless the original thing to a non-UR::Object class so UR::Object::DESTROY # doesn't run on it my $cloned_thing = UR::BoolExpr::Util::clonedThing->bless($_); # Swap in the object from the object cache $_ = $UR::Context::all_objects_loaded->{$class}->{$id}; } } _fixup_ur_objects_from_thawed_data($_); }; foreach my $data ( @values ) { next unless ref $data; # Don't need to recursively inspect normal scalar data next if $seen->{$data}++; if (ref $data) { my $reftype = reftype($data); my $iter; if ($reftype eq 'ARRAY') { foreach (@$data) { &$process_it; } } elsif ($reftype eq 'HASH') { foreach (values %$data) { &$process_it; } } elsif ($reftype eq 'SCALAR' or $reftype eq 'REF') { local $_ = $$data; &$process_it; } } } return @values; } # These are used for the simple common-case rules. sub values_to_value_id { my $value_id = "O:"; for my $value (@_) { no warnings;# 'uninitialized'; if (length($value)) { if (ref($value) eq "ARRAY") { if (@$value == 0) { $value_id .= $empty_list; } else { for my $value2 (@$value) { if (not defined $value2 ) { $value_id .= $null_value . $unit_sep; } elsif ($value2 eq "") { $value_id .= $empty_string . $unit_sep; } else { if (ref($value2) or index($value2, $unit_sep) >= 0 or index($value2, $record_sep) >= 0) { return values_to_value_id_frozen(@_); } $value_id .= $value2 . $unit_sep; } } } $value_id .= $record_sep; } else { if (ref($value) or index($value,$unit_sep) >= 0 or index($value,$record_sep) >= 0) { return values_to_value_id_frozen(@_); } $value_id .= $value . $record_sep; } } elsif (not defined $value ) { $value_id .= $null_value . $record_sep; } else {# ($value eq "") { $value_id .= $empty_string . $record_sep; } } return $value_id; } sub value_id_to_values { my $value_id = shift; unless (defined $value_id) { Carp::confess('No value_id passed in to value_id_to_values()!?'); } my $method_identifier = substr($value_id,0,2); $value_id = substr($value_id, 2, length($value_id)-2); if ($method_identifier eq "F:") { return value_id_to_values_frozen($value_id); } my @values = ($value_id =~ /(.*?)$record_sep/gs); for (@values) { if (substr($_,-1) eq $unit_sep) { #$_ = [split($unit_sep,$_)] my @values2 = /(.*?)$unit_sep/gs; $_ = \@values2; for (@values2) { if ($_ eq $null_value) { $_ = undef; } elsif ($_ eq $empty_string) { $_ = ""; } } } elsif ($_ eq $null_value) { $_ = undef; } elsif ($_ eq $empty_string) { $_ = ""; } elsif ($_ eq $empty_list) { $_ = []; } } return @values; } sub is_meta_param { my $param_name = shift; return substr($param_name, 0, 1) eq '-'; } package UR::BoolExpr::Util::clonedThing; sub bless { my($class, $thing) = @_; # return $thing if ($thing->isa(__PACKAGE__)); $thing->{__original_class} = $thing->class; bless $thing, $class; } sub id { return shift->{id}; } sub class { return shift->{__original_class}; } 1; =pod =head1 NAME UR::BoolExpr::Util - non-OO module to collect utility functions used by the BoolExpr modules =cut Change.pm100664023532023421 1214612544604516 14500 0ustar00abrummetgsc000000000000UR-0.44/lib/UR package UR::Change; use strict; use warnings; use IO::File; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, has => [ changed_class_name => { is => 'String' }, changed_id => { }, changed_aspect => { is => 'String' }, undo_data => { is_optional => 1 }, # Some changes (like create) have no undo data ], is_transactional => 1, ); sub changed_object { my $self = shift; my $changed_obj; my $changed_aspect = $self->changed_aspect; if ($changed_aspect eq "delete" or $changed_aspect eq "unload") { my $undo_data = $self->undo_data; unless (defined $undo_data) { $undo_data = ''; } $changed_obj = eval "no strict; no warnings; " . $undo_data; my $error = $@; bless($changed_obj, 'UR::DeletedRef') if (ref $changed_obj); # changed class so that UR::Object::DESTROY is not called on a "fake" UR::Object if ($error) { Carp::confess("Error reconstructing $changed_aspect data for @_: $error"); } } else { $changed_obj = $self->changed_class_name->get($self->changed_id); } if (defined $changed_obj) { return $changed_obj; } else { return; } } sub undo { my $self = shift; my $changed_class_name = $self->changed_class_name; my $changed_id = $self->changed_id; my $changed_aspect = $self->changed_aspect; my $undo_data = $self->undo_data; if (0) { no warnings; my @k = qw/changed_class_name changed_id changed_aspect undo_data/; my @v = @$self{@k}; print "\tundoing @v\n"; }; # Ghosts are managed internally by create/delete. # Allow reversal of those methods to indirectly reverse ghost changes. if ($changed_class_name =~ /::Ghost/) { if ($changed_aspect !~ /^(create|delete)(_object|)$/) { Carp::confess("Unlogged change on ghost? @_"); } return 1; } # For tracking "external" changes allow the undo to execute a closure if ($changed_aspect eq 'external_change') { if (ref($undo_data) eq 'CODE') { return eval { &$undo_data }; } else { die $self->error_message("'external_change' expects a code ref for undo data!"); } } my $changed_obj = $self->changed_object(); return unless $changed_obj; # TODO: if no changed object, die? if ($changed_aspect eq "__define__") { $changed_obj->unload(); } elsif ($changed_aspect eq "create") { if ($changed_obj->isa('UR::Observer')) { UR::Observer::delete($changed_obj); # Observers have state that needs to be cleaned up } else { UR::Object::delete($changed_obj); } } elsif ($changed_aspect eq "delete") { my %stored; for my $key (keys %$changed_obj) { if ($key =~ /^(status|warning|error|debug)_message$/ or ref($changed_obj->{$key}) ) { $stored{$key} = delete $changed_obj->{$key}; } } $changed_obj = UR::Object::create($changed_class_name,%$changed_obj); for my $key (keys %stored) { $changed_obj->{$key} = $stored{$key}; } $changed_obj->{'_change_count'}--; # it was incremented when delete() was called on the object } elsif ($changed_aspect eq "load") { UR::Object::unload($changed_obj); } elsif ($changed_aspect eq "load_external") { } elsif ($changed_aspect eq "unload") { $changed_obj = $UR::Context::current->_construct_object($changed_class_name,%$changed_obj); UR::Object::__signal_change__($changed_obj,"load") if $changed_obj; } elsif ($changed_aspect eq "commit") { if ($changed_obj->isa('UR::Context::Transaction')) { UR::Object::unload($changed_obj); } else { Carp::confess(); } } elsif ($changed_aspect eq "rollback") { Carp::confess(); } elsif ($changed_aspect eq 'rewrite_module_header') { my $VAR1; eval $undo_data; my $filename = $VAR1->{'path'}; my $data = $VAR1->{'data'}; if (defined $data) { # The file previously existed, restore the old contents my $f = IO::File->new(">$filename"); unless ($f) { Carp::confess("Can't open $filename for writing while undo on rewrite_module_header for class $changed_class_name: $!"); } $f->print($data); $f->close(); } else { # The file did not previously exist, remove the file unlink($filename); } } else { # regular property if ($changed_obj->can($changed_aspect)) { $changed_obj->$changed_aspect($undo_data); $changed_obj->{'_change_count'} -= 2; # 2 because the line above will actually increment the counter, too } } $changed_obj->{'_change_count'} = 0 if ($changed_obj->{'_change_count'} and $changed_obj->{'_change_count'} < 0); return 1; } 1; Context.pm100664023532023421 45637612544604516 15000 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Context; use strict; use warnings; use Sub::Name; use Scalar::Util; require UR; our $VERSION = "0.44"; # UR $VERSION; use UR::Context::ImportIterator; use UR::Context::ObjectFabricator; use UR::Context::LoadingIterator; UR::Object::Type->define( class_name => 'UR::Context', is_abstract => 1, has => [ parent => { is => 'UR::Context', id_by => 'parent_id', is_optional => 1 }, query_underlying_context => { is => 'Boolean', is_optional => 1, default_value => undef, doc => 'Flag indicating whether the context must (1), must not (0) or may (undef) query underlying contexts when handling a query' }, ], valid_signals => [qw(precommit sync_databases commit prerollback rollback)], doc => <get($root_id); unless ($UR::Context::root) { die "Failed to find root context object '$root_id':!? Odd value in environment variable UR_CONTEXT_ROOT?"; } if (my $base_id = $ENV{UR_CONTEXT_BASE}) { $UR::Context::base = UR::Context::Process->get($base_id); unless ($UR::Context::base) { die "Failed to find base context object '$base_id':!? Odd value in environment variable UR_CONTEXT_BASE?"; } } else { $UR::Context::base = $UR::Context::root; } $UR::Context::process = UR::Context::Process->_create_for_current_process(parent_id => $UR::Context::base->id); if (exists $ENV{'UR_CONTEXT_CACHE_SIZE_LOWWATER'} || exists $ENV{'UR_CONTEXT_CACHE_SIZE_HIGHWATER'}) { $UR::Context::destroy_should_clean_up_all_objects_loaded = 1; $cache_size_highwater = $ENV{'UR_CONTEXT_CACHE_SIZE_HIGHWATER'} || 0; $cache_size_lowwater = $ENV{'UR_CONTEXT_CACHE_SIZE_LOWWATER'} || 0; } # This changes when we initiate in-memory transactions on-top of the basic, heavier weight one for the process. $UR::Context::current = $UR::Context::process; if (exists $ENV{'UR_CONTEXT_MONITOR_QUERY'}) { $UR::Context::current->monitor_query($ENV{'UR_CONTEXT_MONITOR_QUERY'}); } $initialized = 1; return $UR::Context::current; } # the current context is either the process context, or the current transaction on-top of it *get_current = \¤t; sub current { return $UR::Context::current; } sub process { return $UR::Context::process; } sub date_template { return q|%Y-%m-%d %H:%M:%S|; } sub now { return Date::Format::time2str(date_template(), time()); } my $master_monitor_query = 0; sub monitor_query { return if $UR::Object::Type::bootstrapping; my $self = shift; $self = $UR::Context::current unless (ref $self); if (@_) { if (ref $self) { $self->{'monitor_query'} = shift; } else { $master_monitor_query = shift; } } return ref($self) ? $self->{'monitor_query'} : $master_monitor_query; } my %_query_log_times; my $query_logging_fh = IO::Handle->new(); $query_logging_fh->fdopen(fileno(STDERR), 'w'); $query_logging_fh->autoflush(1); sub query_logging_fh { $query_logging_fh = $_[1] if @_ > 1; return $query_logging_fh; } sub _log_query_for_rule { return if $UR::Object::Type::bootstrapping; my $self = shift; my($subject_class,$rule,$message) = @_; my $monitor_level; return unless ($monitor_level = $self->monitor_query); return if (substr($subject_class, 0,4) eq 'UR::' and $monitor_level < 2); # Don't log queries for internal classes my $elapsed_time = 0; if (defined($rule)) { my $time_now = Time::HiRes::time(); if (! exists $_query_log_times{$rule->id}) { $_query_log_times{$rule->id} = $time_now; } else { $elapsed_time = $time_now - $_query_log_times{$rule->id}; } } if ($elapsed_time) { $message .= sprintf(" Elapsed %.4f s", $elapsed_time); } $query_logging_fh->print($message."\n"); } sub _log_done_elapsed_time_for_rule { my($self, $rule) = @_; delete $_query_log_times{$rule->id}; } sub resolve_data_sources_for_class_meta_and_rule { my $self = shift; my $class_meta = shift; my $boolexpr = shift; ## ignored in the default case my $class_name = $class_meta->class_name; # These are some hard-coded cases for splitting up class-classes # and data dictionary entities into namespace-specific meta DBs. # Maybe there's some more generic way to move this somewhere else # FIXME This part is commented out for the moment. When class info is in the # Meta DBs, then try getting this to work #if ($class_name eq 'UR::Object::Type') { # my %params = $boolexpr->legacy_params_hash; # my($namespace) = ($params->{'class_name'} =~ m/^(\w+?)::/); # $namespace ||= $params->{'class_name'}; # In case the class name is just the namespace # # return $namespace . '::DataSource::Meta'; #} my $data_source; # For data dictionary items # When the FileMux datasource is more generalized and works for # any kind of underlying datasource, this code can move from here # and into the base class for Meta datasources if ($class_name->isa('UR::DataSource::RDBMS::Entity')) { my $params = $boolexpr->legacy_params_hash; my $namespace; if ($params->{'namespace'}) { $namespace = $params->{'namespace'}; $data_source = $params->{'namespace'} . '::DataSource::Meta'; } elsif ($params->{'data_source'} && ! ref($params->{'data_source'}) && $params->{'data_source'}->can('get_namespace')) { $namespace = $params->{'data_source'}->get_namespace; $data_source = $namespace . '::DataSource::Meta'; } elsif ($params->{'data_source'} && ref($params->{'data_source'}) eq 'ARRAY') { my %namespaces = map { $_->get_namespace => 1 } @{$params->{'data_source'}}; unless (scalar(keys %namespaces) == 1) { Carp::confess("get() across multiple namespaces is not supported"); } $namespace = $params->{'data_source'}->[0]->get_namespace; $data_source = $namespace . '::DataSource::Meta'; } else { Carp::confess("Required parameter (namespace or data_source_id) missing"); #$data_source = 'UR::DataSource::Meta'; } if (my $exists = UR::Object::Type->get($data_source)) { # switch the terminology above to stop using $data_source for the class name # now it's the object.. $data_source = $data_source->get(); } else { $self->warning_message("no data source $data_source: generating for $namespace..."); UR::DataSource::Meta->generate_for_namespace($namespace); $data_source = $data_source->get(); } unless ($data_source) { Carp::confess "Failed to find or generate a data source for meta data for namespace $namespace!"; } } else { $data_source = $class_meta->data_source; } if ($data_source) { $data_source = $data_source->resolve_data_sources_for_rule($boolexpr); } return $data_source; } # this is used to determine which data source an object should be saved-to sub resolve_data_source_for_object { my $self = shift; my $object = shift; my $class_meta = $object->__meta__; my $class_name = $class_meta->class_name; if ($class_name->isa('UR::DataSource::RDBMS::Entity') || $class_name->isa('UR::DataSource::RDBMS::Entity::Ghost')) { my $data_source = $object->data_source; my($namespace) = ($data_source =~ m/(^\w+?)::DataSource/); unless ($namespace) { Carp::croak("Can't resolve data source for object of type $class_name: The object's namespace could not be inferred from its data_source $data_source"); } my $ds_name = $namespace . '::DataSource::Meta'; return $ds_name->get(); } # Default behavior my $ds = $class_meta->data_source; return $ds; } # this turns on and off light caching (weak refs) sub _light_cache { if (@_ > 1) { $UR::Context::light_cache = $_[1]; $UR::Context::destroy_should_clean_up_all_objects_loaded = $UR::Context::light_cache; } return $UR::Context::light_cache; } # Given a rule, and a property name not mentioned in the rule, # can we infer the value of that property from what actually is in the rule? sub infer_property_value_from_rule { my($self,$wanted_property_name,$rule) = @_; # First, the easy case... The property is directly mentioned in the rule if ($rule->specifies_value_for($wanted_property_name)) { return $rule->value_for($wanted_property_name); } my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = UR::Object::Type->get($subject_class_name); my $wanted_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_name); unless ($wanted_property_meta) { $self->error_message("Class $subject_class_name has no property named $wanted_property_name"); return; } if ($wanted_property_meta->is_delegated) { $self->context_return($self->_infer_delegated_property_from_rule($wanted_property_name,$rule)); } else { $self->context_return($self->_infer_direct_property_from_rule($wanted_property_name,$rule)); } } # These are things that are changes to the program state, but not changes to the object instance # so they shouldn't be counted in the object's change_count my %changes_not_counted = map { $_ => 1 } qw(load define unload query connect); sub add_change_to_transaction_log { my ($self,$subject, $property, @data) = @_; my ($class,$id); if (ref($subject)) { $class = ref($subject); $id = $subject->id; unless ($changes_not_counted{$property} ) { $subject->{_change_count}++; #print "changing $subject $property @data\n"; } } else { $class = $subject; $subject = undef; $id = undef; } if ($UR::Context::Transaction::log_all_changes) { # eventually all calls to __signal_change__ will go directly here UR::Context::Transaction->log_change($subject, $class, $id, $property, @data); } if (my $index_list = $UR::Object::Index::all_by_class_name_and_property_name{$class}{$property}) { unless ($property eq 'create' or $property eq 'load' or $property eq 'define') { for my $index (@$index_list) { $index->_remove_object( $subject, { $property => $data[0] } ) } } unless ($property eq 'delete' or $property eq 'unload') { for my $index (@$index_list) { $index->_add_object($subject) } } } } our $sig_depth = 0; my %subscription_classes; sub send_notification_to_observers { my ($self,$subject, $property, @data) = @_; my ($class,$id); if (ref($subject)) { $class = ref($subject); $id = $subject->id; } else { $class = $subject; } my $check_classes = $subscription_classes{$class}; unless ($check_classes) { $subscription_classes{$class} = $check_classes = [ $class ? ( $class, (grep { $_->isa("UR::Object") } $class->inheritance), '' ) : ('') ]; } my @check_properties = ($property ? ($property, '') : ('') ); my @check_ids = (defined($id) ? ($id, '') : ('') ); my @matches = map { @$_ } grep { defined $_ } map { defined($id) ? @$_{@check_ids} : values(%$_) } grep { defined $_ } map { @$_{@check_properties} } grep { defined $_ } @$UR::Context::all_change_subscriptions{@$check_classes}; return unless @matches; $sig_depth++; if (@matches > 1) { no warnings; # sort by priority @matches = sort { $a->[2] <=> $b->[2] } @matches; }; foreach my $callback_info (@matches) { my ($callback, $note, undef, $id, $once) = @$callback_info; UR::Observer->get($id)->delete() if $once; $callback->($subject, $property, @data); } $sig_depth--; return scalar(@matches); } sub query { my $self = shift; # Fast optimization for the default case. if ( ( !ref($self) or ! $self->query_underlying_context) and ! Scalar::Util::blessed($_[1]) # This happens when query() is called with a class name and boolexpr ) { no warnings; if (exists $UR::Context::all_objects_loaded->{$_[0]}) { my $is_monitor_query = $self->monitor_query; if (defined(my $obj = $UR::Context::all_objects_loaded->{$_[0]}->{$_[1]})) { # Matched the class and ID directly - pull it right out of the cache if ($is_monitor_query) { $self->_log_query_for_rule($_[0], undef, Carp::shortmess("QUERY: class $_[0] by ID $_[1]")); $self->_log_query_for_rule($_[0], undef, "QUERY: matched 1 cached object\nQUERY: returning 1 object\n\n"); } $obj->{'__get_serial'} = $UR::Context::GET_COUNTER++; return $obj; } elsif (my $subclasses = $UR::Object::Type::_init_subclasses_loaded{$_[0]}) { # Check subclasses of the requested class, along with the ID # yes, it only goes one level deep. This should catch enough cases to be worth it. # Deeper searches will be covered by get_objects_for_class_and_rule() foreach my $subclass (@$subclasses) { if (exists $UR::Context::all_objects_loaded->{$subclass} and my $obj = $UR::Context::all_objects_loaded->{$subclass}->{$_[1]} ) { if ($is_monitor_query) { $self->_log_query_for_rule($_[0], undef, Carp::shortmess("QUERY: class $_[0] by ID $_[1]")); $self->_log_query_for_rule($_[0], undef, "QUERY: matched 1 cached object in subclass $subclass\nQUERY: returning 1 object\n\n"); } $obj->{'__get_serial'} = $UR::Context::GET_COUNTER++; return $obj; } } } } }; # Normal logic for finding objects smartly is below. my $class = shift; # Handle the case in which this is called as an object method. # Functionality is completely different. if(ref($class)) { my @rvals; foreach my $prop (@_) { push(@rvals, $class->$prop()); } if(wantarray) { return @rvals; } else { return \@rvals; } } my ($rule, @extra) = UR::BoolExpr->resolve($class,@_); if (@extra) { # remove this and have the developer go to the datasource if (scalar @extra == 2 and ($extra[0] eq "sql" or $extra[0] eq 'sql in')) { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } # keep this part: let the sub-class handle special params if it can return $class->get_with_special_parameters($rule, @extra); } # This is here for bootstrapping reasons: we must be able to load class singletons # in order to have metadata for regular loading.... # UR::DataSource::QueryPlan isa UR::Value (which has custom loading logic), but we need to be able to generate # a QueryPlan independant of the normal loading process, otherwise there'd be endless recursion (Can't generate a QueryPlan # for a QueryPlan without generating a QueryPlan first....) if (!$rule->has_meta_options and ($class->isa("UR::Object::Type") or $class->isa("UR::Singleton") or $class->isa("UR::DataSource::QueryPlan"))) { my $normalized_rule = $rule->normalize; my @objects = $class->_load($normalized_rule); return unless defined wantarray; return @objects if wantarray; if ( @objects > 1 and defined(wantarray)) { Carp::croak("Multiple matches for $class query called in scalar context. $rule matches " . scalar(@objects). " objects"); } return $objects[0]; } return $UR::Context::current->get_objects_for_class_and_rule($class, $rule); } sub _resolve_id_for_class_and_rule { my ($self,$class_meta,$rule) = @_; my $class = $class_meta->class_name; my $id; my @id_property_names = $class_meta->id_property_names or Carp::confess( # Bad should be at least one "No id property names for class ($class). This should not have happened." ); if ( @id_property_names == 1 ) { # only 1 - try to auto generate $id = $class_meta->autogenerate_new_object_id($rule); unless ( defined $id ) { $class->error_message("Failed to auto-generate an ID for single ID property class ($class)"); return; } } else { # multiple # Try to give a useful message by getting id prop names that are not deinfed my @missed_names; for my $name ( @id_property_names ) { push @missed_names, $name unless $rule->specifies_value_for($name); } if ( @missed_names ) { # Ok - prob w/ class def, list the ones we missed $class->error_message("Attempt to create $class with multiple ids without these properties: ".join(', ', @missed_names)); return; } else { # Bad - something is really wrong... Carp::confess("Attempt to create $class failed to resolve id from underlying id properties."); } } return $id; } our $construction_method = 'create'; # Pulled out the complicated code of create_entity() below that deals with # abstract classes and subclassify_by sub _create_entity_from_abstract_class { my $self = shift; my $class = shift; my $class_meta = $class->__meta__; my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, @_); # If we can easily determine the correct subclass, delegate to that subclass' create() my $subclassify_by = $class_meta->subclassify_by(); unless (defined $subclassify_by) { Carp::croak("Can't call $construction_method on abstract class $class without a subclassify_by property"); } my $sub_class_name = $rule->value_for($subclassify_by); unless (defined $sub_class_name) { # The subclassification wasn't included in the rule my $property_meta = $class_meta->property($subclassify_by); unless ($property_meta) { Carp::croak("Abstract class $class has subclassify_by $subclassify_by, but no property exists by that name"); } # There are a few different ways the property can supply a value for subclassify_by... # The sure-fire way to get a value is to go ahead an instantiate the object into the # base/abstract class, and then we can just call the property as a method. There's # a lot of overhead in that, so first we'll try some of the easier, common-case ways if ($property_meta->default_value) { # The property has a default value $sub_class_name = $property_meta->default_value(); } elsif ($property_meta->is_calculated and ref($property_meta->calculate) eq 'CODE') { # It's calculated via a coderef my $calculate_from = $property_meta->calculate_from; my @calculate_params; foreach my $prop_name ( @$calculate_from ) { # The things in calculate_from must appear in the rule unless ($rule->specifies_value_for($prop_name)) { Carp::croak("Class $class subclassify_by calculation property '$subclassify_by' " . "requires '$prop_name' in the $construction_method() params\n" . "Params were: " . UR::Util->display_string_for_params_list($rule->params_list)); } push @calculate_params, $rule->value_for($prop_name); } my $sub = $property_meta->calculate; unless ($sub) { Carp::croak("Can't use undefined value as subroutine reference while resolving " . "value for class $class calculated property '$subclassify_by'"); } $sub_class_name = $sub->(@calculate_params); } elsif ($property_meta->is_calculated and !ref($property_meta->calculate)) { # It's calculated via a string that's eval-ed Carp::croak("Can't use a non-coderef as a calculation for class $class subclassify_by"); } elsif ($property_meta->is_delegated) { #Carp::croak("Delegated properties are not supported for subclassifying $class with property '$subclassify_by'"); my @values = $self->infer_property_value_from_rule($subclassify_by, $rule); if (! @values ) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Couldn't infer a value for indirect property '$subclassify_by' via rule $rule"); } elsif (@values > 1) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Infering a value for property '$subclassify_by' via rule $rule returned multiple values: " . join(', ', @values)); } else { $sub_class_name = $values[0]; } } else { Carp::croak("Can't use undefined value as a subclass name for $class property '$subclassify_by'"); } } unless (defined $sub_class_name) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Can't use undefined value as a subclass name for param '$subclassify_by'"); } if ($sub_class_name eq $class) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Value for $subclassify_by cannot be the same as the original class"); } unless ($sub_class_name->isa($class)) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Class $sub_class_name is not a subclass of $class"); } return $sub_class_name->$construction_method(@_); } my %memos; my %memos2; sub create_entity { my $self = shift; my $class = shift; my $memo = $memos{$class}; unless ($memo) { # we only want to grab the data necessary for object construction once # this occurs the first time a new object is created for a given class my $class_meta = $class->__meta__; my @inheritance = reverse ($class_meta, $class_meta->ancestry_class_metas); # %property_objects maps property names to UR::Object::Property objects # by going through the reversed list of UR::Object::Type objects below # We set up this hash to have the correct property objects for each property # name. This is important in the case of property name overlap via # inheritance. The property object used should be the one "closest" # to the class. In other words, a property directly on the class gets # used instead of an inherited one. my %property_objects; my %direct_properties; my %indirect_properties; my %set_properties; my %default_values; my %default_value_requires_query; my %default_value_requires_call; my %immutable_properties; my @deep_copy_default_values; for my $co ( @inheritance ) { # Reverse map the ID into property values. # This has to occur for all subclasses which represent table rows. # deal with %property_objects my @property_objects = $co->direct_property_metas; my @property_names = map { $_->property_name } @property_objects; @property_objects{@property_names} = @property_objects; foreach my $prop ( @property_objects ) { my $name = $prop->property_name; unless (defined $name) { Carp::confess("no name on property for class " . $co->class_name . "?\n" . Data::Dumper::Dumper($prop)); } my $default_value = $prop->default_value; if (defined $default_value) { if ($prop->data_type and $prop->_data_type_as_class_name eq $prop->data_type and $prop->_data_type_as_class_name->can("get")) { # an ID or other query params in hash/array form return an object or objects $default_value_requires_query{$name} = $default_value; } elsif (ref($default_value)) { #warn ( # "a reference value $default_value is used as a default on " # . $co->class_name # . " forcing a copy during construction " # . " of $class $name..." #); push @deep_copy_default_values, $name; } $default_values{$name} = $default_value; } if ($prop->calculated_default) { $default_value_requires_call{$name} = $prop->calculated_default; } if ($prop->is_many) { $set_properties{$name} = $prop; } elsif ($prop->is_delegated) { $indirect_properties{$name} = $prop; } else { $direct_properties{$name} = $prop; } unless ($prop->is_mutable) { $immutable_properties{$name} = 1; } } } my @indirect_property_names = keys %indirect_properties; my @direct_property_names = keys %direct_properties; my @subclassify_by_methods; foreach my $co ( @inheritance ) { # If this class inherits from something with subclassify_by, make sure the param # actually matches. If it's not supplied, then set it to the same as the class create() # is being called on if ( $class ne $co->class_name and $co->is_abstract and my $method = $co->subclassify_by ) { push @subclassify_by_methods, $method; } } $memos{$class} = $memo = [ $class_meta, $class_meta->first_sub_classification_method_name, $class_meta->is_abstract, \@inheritance, \%property_objects, \%direct_properties, \%indirect_properties, \%set_properties, \%immutable_properties, \@subclassify_by_methods, \%default_values, (@deep_copy_default_values ? \@deep_copy_default_values : undef), \%default_value_requires_query, \%default_value_requires_call, ]; } my ( $class_meta, $first_sub_classification_method_name, $is_abstract, $inheritance, $property_objects, $direct_properties, $indirect_properties, $set_properties, $immutable_properties, $subclassify_by_methods, $initial_default_values, $deep_copy_default_values, $default_value_requires_query, $initial_default_value_requires_call, ) = @$memo; # The old way of automagic subclassing... # The class specifies that we should call a class method (sub_classification_method_name) # to determine the correct subclass if ($first_sub_classification_method_name) { my $sub_class_name = $class->$first_sub_classification_method_name(@_); if (defined($sub_class_name) and ($sub_class_name ne $class)) { # delegate to the sub-class to create the object unless ($sub_class_name->can($construction_method)) { Carp::croak("Can't locate object method '$construction_method' via package '$sub_class_name' " . "while resolving proper subclass for $class during $construction_method"); } return $sub_class_name->$construction_method(@_); } # fall through if the class names match } if ($is_abstract) { # The new way of automagic subclassing. The class specifies a property (subclassify_by) # that holds/returns the correct subclass name return $self->_create_entity_from_abstract_class($class, @_); } # normal case: make a rule out of the passed-in params # rather than normalizing the rule, we just do the extension part which is fast my $rule = UR::BoolExpr->resolve($class, @_); my $template = $rule->template; my $params = { $rule->_params_list, $template->extend_params_list_for_values(@{$rule->{values}}) }; if (my $a = $template->{_ambiguous_keys}) { my $p = $template->{_ambiguous_property_names}; @$params{@$p} = delete @$params{@$a}; } my $id = $params->{id}; unless (defined $id) { $id = $self->_resolve_id_for_class_and_rule($class_meta,$rule); unless ($id) { return; } $rule = UR::BoolExpr->resolve_normalized($class, %$params, id => $id); $params = { $rule->params_list }; ; } my %default_value_requires_call = %$initial_default_value_requires_call; delete @default_value_requires_call{ keys %$params }; # handle postprocessing default values my %default_values = %$initial_default_values; #for my $name (qw//) { for my $name (keys %$default_value_requires_query) { my @id_by; if (my $id_by = $property_objects->{$name}->id_by) { @id_by = (ref($id_by) ? @$id_by : ($id_by)); } if ($params->{$name}) { delete $default_values{$name}; } elsif (@$params{@id_by}) { # some or all of the id is present # don't fall back to the default for my $id_by (@id_by) { delete $default_values{$id_by} if exists $params->{$id_by}; } delete $default_values{$name}; } else { $DB::single = 1; my $query = $default_value_requires_query->{$name}; my @query; if (ref($query) eq 'HASH') { # queries come in as a hash @query = %$query; } else { # an ID or a boolean expression @query = ($query); } my $prop = $property_objects->{$name}; my $class = $prop->_data_type_as_class_name; eval { if ($prop->is_many) { $default_values{$name} = [ $class->get(@query) ]; } else { $default_values{$name} = $class->get(@query); } }; if ($@) { warn "error setting " . $prop->class_name . " " . $prop->property_name . " to default_value from query $query for type $class!"; }; } } if ($deep_copy_default_values) { for my $name (@$deep_copy_default_values) { if ($params->{$name}) { delete $default_values{$name}; } else { $default_values{$name} = UR::Util::deep_copy($default_values{$name}); } } } # @extra is extra values gotten by inheritance my @extra; my $indirect_values = {}; for my $property_name (keys %$indirect_properties) { # pull indirect values out of the constructor hash # so we can apply them separately after making the object if ( exists $params->{ $property_name } ) { $indirect_values->{ $property_name } = delete $params->{ $property_name }; delete $default_values{$property_name}; } elsif (exists $default_values{$property_name}) { $indirect_values->{ $property_name } = delete $default_values{$property_name}; } } # if the indirect property is immutable, but it is via something which is # mutable, we use those values to get or create the bridge. my %indirect_immutable_properties_via; for my $property_name (keys %$indirect_values) { if ($immutable_properties->{$property_name}) { my $meta = $indirect_properties->{$property_name}; next unless $meta; # not indirect my $via = $meta->via; next unless $via; # not a via/to (id_by or reverse_id_by) $indirect_immutable_properties_via{$via}{$property_name} = delete $indirect_values->{$property_name}; } } for my $via (keys %indirect_immutable_properties_via) { my $via_property_meta = $class_meta->property_meta_for_name($via); my ($source_indirect_property, $source_value) = each %{$indirect_immutable_properties_via{$via}}; # There'll only ever be one key/value unless ($via_property_meta) { Carp::croak("No metadata for class $class property $via while resolving indirect value for property $source_indirect_property"); } my $indirect_property_meta = $class_meta->property_meta_for_name($source_indirect_property); unless ($indirect_property_meta) { Carp::croak("No metadata for class $class property $source_indirect_property while resolving indirect value for property $source_indirect_property"); } unless ($indirect_property_meta->to) { # We're probably dealing with a subclassify_by property where the subclass has # implicitly overridden the indirect property in the parent class with a constant-value # property in the subclass. Try asking the parent class about a property of the same name ($indirect_property_meta) = grep { $_->property_name eq $indirect_property_meta->property_name } $class_meta->ancestry_property_metas(); unless ($indirect_property_meta and $indirect_property_meta->to) { Carp::croak("Can't resolve indirect relationship for possibly overridden property '$source_indirect_property'" . " in class $class. Parent classes have no property named '$source_indirect_property'"); } } my $foreign_class = $via_property_meta->data_type; my $foreign_property = $indirect_property_meta->to; my $foreign_object = $foreign_class->get($foreign_property => $source_value); unless ($foreign_object) { # This will trigger recursion back here (into create_entity() ) if this property is multiply # indirect, such as through a bridge object $foreign_object = $foreign_class->create($foreign_property => $source_value); unless ($foreign_object) { Carp::croak("Can't create object of class $foreign_class with params ($foreign_property => '$source_value')" . " while resolving indirect value for class $class property $source_indirect_property"); } } my @joins = $indirect_property_meta->_resolve_join_chain(); my %local_properties_to_set; foreach my $join ( @joins ) { if ($join->{foreign_class}->isa("UR::Value")) { # this final "join" is to the set of values available to the raw primitive type # ...not what we really mean by delegation next; } for (my $i = 0; $i < @{$join->{'source_property_names'}}; $i++) { my $source_property_name = $join->{'source_property_names'}->[$i]; next unless (exists $direct_properties->{$source_property_name}); my $foreign_property_name = $join->{'foreign_property_names'}->[$i]; my $value = $foreign_object->$foreign_property_name; if ($rule->specifies_value_for($source_property_name) and $rule->value_for($source_property_name) ne $value) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Conflicting values for property '$source_property_name'. $construction_method rule " . "specifies value '" . $rule->value_for($source_property_name) . "' but " . "indirect immutable property '$source_indirect_property' with value " . "$source_value requires it to be '$value'"); } $local_properties_to_set{$source_property_name} = $value; } } # transfer the values we resolved back into %$params my @param_keys = keys %local_properties_to_set; @$params{@param_keys} = @local_properties_to_set{@param_keys}; } my $set_values = {}; for my $property_name (keys %$set_properties) { if (exists $params->{ $property_name }) { delete $default_values{ $property_name }; $set_values->{ $property_name } = delete $params->{ $property_name }; } } my $entity = $self->_construct_object($class, %default_values, %$params, @extra); return unless $entity; $self->add_change_to_transaction_log($entity, $construction_method); $self->add_change_to_transaction_log($entity, 'load') if $construction_method eq '__define__'; for my $property_name ( keys %default_value_requires_call ) { my $method = $default_value_requires_call{$property_name}; my $value = $method->($entity); $entity->$property_name($value); } # If a property is calculated + immutable, and it wasn't supplied in the params, # that means we need to run the calculation once and store the value in the # object as a read-only attribute foreach my $property_name ( keys %$immutable_properties ) { my $property_meta = $property_objects->{$property_name}; if (!exists($params->{$property_name}) and $property_meta and $property_meta->is_calculated) { my $value = $entity->$property_name; $params->{$property_name} = $value; } } for my $subclassify_by (@$subclassify_by_methods) { my $param_value = $rule->value_for($subclassify_by); $param_value = eval { $entity->$subclassify_by } unless (defined $param_value); $param_value = $default_values{$subclassify_by} unless (defined $param_value); if (! defined $param_value) { # This should have been taken care of by the time we got here... Carp::croak("Invalid parameters for $class->$construction_method(): " . "Can't use an undefined value as a subclass name for param '$subclassify_by'"); } elsif ($param_value ne $class) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Value for subclassifying param '$subclassify_by' " . "($param_value) does not match the class it was called on ($class)"); } } # add items for any multi properties if (%$set_values) { for my $property_name (keys %$set_values) { my $meta = $set_properties->{$property_name}; my $singular_name = $meta->singular_name; my $adder = 'add_' . $singular_name; my $value = $set_values->{$property_name}; unless (ref($value) eq 'ARRAY') { $value = [$value]; } for my $item (@$value) { if (ref($item) eq 'ARRAY') { $entity->$adder(@$item); } elsif (ref($item) eq 'HASH') { $entity->$adder(%$item); } else { $entity->$adder($item); } } } } # set any indirect mutable properties if (%$indirect_values) { for my $property_name (keys %$indirect_values) { $entity->$property_name($indirect_values->{$property_name}); } } if (%$immutable_properties) { my @problems = $entity->__errors__(); if (@problems) { my @errors_fatal_to_construction; my %problems_by_property_name; for my $problem (@problems) { my @problem_properties; for my $name ($problem->properties) { if ($immutable_properties->{$name}) { push @problem_properties, $name; } } if (@problem_properties) { push @errors_fatal_to_construction, join(" and ", @problem_properties) . ': ' . $problem->desc; } } if (@errors_fatal_to_construction) { my $msg = 'Failed to $construction_method ' . $class . ' with invalid immutable properties:' . join("\n", @errors_fatal_to_construction); } } } $entity->__signal_observers__($construction_method); $entity->__signal_observers__('load') if $construction_method eq '__define__'; $entity->{'__get_serial'} = $UR::Context::GET_COUNTER++; $UR::Context::all_objects_cache_size++; return $entity; } sub _construct_object { my $self = shift; my $class = shift; my $params = { @_ }; my $id = $params->{id}; unless (defined($id)) { Carp::confess( "No ID specified (or incomplete id params) for $class _construct_object. Params were:\n" . Data::Dumper::Dumper($params) ); } if ($UR::Context::all_objects_loaded->{$class}->{$id}) { # The object exists. This is not an exception for some reason? # We just return false to indicate that the object is not creatable. $class->error_message("An object of class $class already exists with id value '$id'"); return; } my $object; if ($object = $UR::DeletedRef::all_objects_deleted->{$class}->{$id}) { UR::DeletedRef->resurrect($object); %$object = %$params; } else { $object = bless $params, $class; } if (my $ghost = $UR::Context::all_objects_loaded->{$class . "::Ghost"}->{$id}) { # we're making something which was previously deleted and is pending save. # we must capture the old db_committed data to ensure eventual saving is done correctly. # note this object's database state in the new object so saves occurr correctly, # as an update instead of an insert. if (my $committed_data = $ghost->{db_committed}) { $object->{db_committed} = { %$committed_data }; } if (my $unsaved_data = $ghost->{'db_saved_uncommitted'}) { $object->{'db_saved_uncommitted'} = { %$unsaved_data }; } $ghost->__signal_change__("delete"); $self->_abandon_object($ghost); } # put the object in the master repository of objects for the application. $UR::Context::all_objects_loaded->{$class}{$id} = $object; # If we're using a light cache, weaken the reference. if ($UR::Context::light_cache) { # and substr($class,0,5) ne 'App::') { Scalar::Util::weaken($UR::Context::all_objects_loaded->{$class}->{$id}); } return $object; } sub delete_entity { my ($self,$entity) = @_; if (ref($entity)) { # Delete the specified object. if ($entity->{db_committed} || $entity->{db_saved_uncommitted}) { # gather params for the ghost object my $do_data_source; my %ghost_params; #my @pn; #{ no warnings 'syntax'; # @pn = grep { $_ ne 'data_source_id' || ($do_data_source=1 and 0) } # yes this really is '=' and not '==' # grep { exists $entity->{$_} } # $entity->__meta__->all_property_names; #} my(@prop_names, @many_prop_names); foreach my $prop_name ( $entity->__meta__->all_property_names) { next unless exists $entity->{$prop_name}; # skip non-directly-stored properties if ($prop_name eq 'data_source_id') { $do_data_source = 1; next; } if (ref($entity->{$prop_name}) eq 'ARRAY') { push @many_prop_names, $prop_name; } else { push @prop_names, $prop_name; } } # we're not really allowed to interrogate the data_source property directly @ghost_params{@prop_names} = $entity->get(@prop_names); # hrm doesn't work for is_many properties :( foreach my $prop_name ( @many_prop_names ) { my @values = $entity->get($prop_name); $ghost_params{$prop_name} = \@values; } if ($do_data_source) { $ghost_params{'data_source_id'} = $entity->{'data_source_id'}; } # create ghost object my $ghost = $self->_construct_object($entity->ghost_class, id => $entity->id, %ghost_params); unless ($ghost) { Carp::confess("Failed to constructe a deletion record for an unsync'd delete."); } $ghost->__signal_change__("create"); for my $com (qw(db_committed db_saved_uncommitted)) { $ghost->{$com} = $entity->{$com} if $entity->{$com}; } } $entity->__signal_change__('delete'); $self->_abandon_object($entity); return $entity; } else { Carp::confess("Can't call delete as a class method."); } } sub _abandon_object { my $self = shift; my $object = $_[0]; my $class = $object->class; my $id = $object->id; if ($object->{'__get_serial'}) { # Keep a correct accounting of objects. This one is getting deleted by a method # other than UR::Context::prune_object_cache $UR::Context::all_objects_cache_size--; } # Remove the object from the main hash. delete $UR::Context::all_objects_loaded->{$class}->{$id}; delete $UR::Context::all_objects_are_loaded->{$class}; # Remove all of the load info it is using so it'll get re-loaded if asked for later if ($object->{'__load'}) { while (my ($template_id, $rules) = each %{ $object->{'__load'}} ) { foreach my $rule_id ( keys %$rules ) { delete $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; foreach my $fabricator ( UR::Context::ObjectFabricator->all_object_fabricators ) { $fabricator->delete_from_all_params_loaded($template_id, $rule_id); } } } } # Turn our $object reference into a UR::DeletedRef. # Further attempts to use it will result in readable errors. # The object can be resurrected. if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DELETE object $object class ",$object->class," id ",$object->id,"\n"; } UR::DeletedRef->bury($object); return $object; } # This one works when the rule specifies the value of an indirect property, and we want # the value of a direct property of the class sub _infer_direct_property_from_rule { my($self,$wanted_property_name,$rule) = @_; my $rule_template = $rule->template; my @properties_in_rule = $rule_template->_property_names; # FIXME - why is this method private? my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = $subject_class_name->__meta__; my($alternate_class,$alternate_get_property, $alternate_wanted_property); my @r_values; # There may be multiple properties in the rule that will get to the wanted property PROPERTY_IN_RULE: foreach my $property_name ( @properties_in_rule) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); my $final_property_meta = $property_meta->final_property_meta || $property_meta; $alternate_get_property = $final_property_meta->property_name; $alternate_class = $final_property_meta->class_name; unless ($alternate_wanted_property) { # Either this was also a direct property of the rule, or there's no # obvious link between the indirect property and the wanted property. # the caller probably just should have done a get() $alternate_wanted_property = $wanted_property_name; $alternate_get_property = $property_name; $alternate_class = $subject_class_name; } my $value_from_rule = $rule->value_for($property_name); my @alternate_values; eval { # Inside an eval in case the get() throws an exception, the next # property in the rule may succeed my @alternate_objects = $self->query($alternate_class, $alternate_get_property => $value_from_rule ); @alternate_values = map { $_->$alternate_wanted_property } @alternate_objects; }; next unless (@alternate_values); push @r_values, \@alternate_values; } if (@r_values == 0) { # no solutions found return; } elsif (@r_values == 1) { # there was only one solution return @{$r_values[0]}; } else { # multiple solutions. Only return the intersection of them all # FIXME - this totally won't work for properties that return objects, listrefs or hashrefs # FIXME - this only works for AND rules - for now, that's all that exist my %intersection = map { $_ => 1 } @{ shift @r_values }; foreach my $list ( @r_values ) { %intersection = map { $_ => 1 } grep { $intersection{$_} } @$list; } return keys %intersection; } } # we want the value of a delegated property, and the rule specifies # a direct value sub _infer_delegated_property_from_rule { my($self, $wanted_property_name, $rule) = @_; my $rule_template = $rule->template; my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = $subject_class_name->__meta__; my $wanted_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_name); unless ($wanted_property_meta->via) { Carp::croak("There is no linking meta-property (via) on property $wanted_property_name on $subject_class_name"); } my $linking_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_meta->via); my $final_property_meta = $wanted_property_meta->final_property_meta; if ($linking_property_meta->reverse_as) { eval{ $linking_property_meta->data_type->class() }; # Load the class if it isn't already loaded if ($linking_property_meta->data_type ne $final_property_meta->class_name) { Carp::croak("UR::Context::_infer_delegated_property_from_rule() doesn't handle multiple levels of indiretion yet"); } } my @rule_translation = $linking_property_meta->get_property_name_pairs_for_join(); my %alternate_get_params; foreach my $pair ( @rule_translation ) { my $rule_param = $pair->[0]; next unless ($rule_template->specifies_value_for($rule_param)); my $alternate_param = $pair->[1]; my $value = $rule->value_for($rule_param); $alternate_get_params{$alternate_param} = $value; } my $alternate_class = $final_property_meta->class_name; my $alternate_wanted_property = $wanted_property_meta->to; my @alternate_values; eval { my @alternate_objects = $self->query($alternate_class, %alternate_get_params); @alternate_values = map { $_->$alternate_wanted_property } @alternate_objects; }; return @alternate_values; } sub object_cache_size_highwater { my $self = shift; if (@_) { my $value = shift; $cache_size_highwater = $value; if (defined $value) { if ($cache_size_lowwater and $value <= $cache_size_lowwater) { Carp::confess("Can't set the highwater mark less than or equal to the lowwater mark"); return; } $UR::Context::destroy_should_clean_up_all_objects_loaded = 1; $self->prune_object_cache(); } else { # turn it off $UR::Context::destroy_should_clean_up_all_objects_loaded = 0; } } return $cache_size_highwater; } sub object_cache_size_lowwater { my $self = shift; if (@_) { my $value = shift; $cache_size_lowwater = $value; if (defined($value) and $cache_size_highwater and $value >= $cache_size_highwater) { Carp::confess("Can't set the lowwater mark greater than or equal to the highwater mark"); return; } } return $cache_size_lowwater; } sub get_data_sources_for_loaded_classes { my $class = shift; my %data_source_for_class; foreach my $class ( keys %$UR::Context::all_objects_loaded ) { next if (substr($class,0,-6) eq '::Type'); # skip class objects next unless exists $UR::Context::all_objects_loaded->{$class . '::Type'}; my $class_meta = $UR::Context::all_objects_loaded->{$class . '::Type'}->{$class}; next unless $class_meta; next unless ($class_meta->is_uncachable()); $data_source_for_class{$class} = $class_meta->data_source_id; } return %data_source_for_class; } our $is_pruning = 0; sub prune_object_cache { my $self = shift; return if ($is_pruning); # Don't recurse into here return if (!defined($cache_size_highwater) or !defined($cache_size_lowwater)); return unless ($all_objects_cache_size > $cache_size_highwater); $is_pruning = 1; my $t1; if ($ENV{'UR_DEBUG_OBJECT_RELEASE'} || $ENV{'UR_DEBUG_OBJECT_PRUNING'}) { $t1 = Time::HiRes::time(); print STDERR Carp::longmess("MEM PRUNE begin at $t1 ",scalar(localtime($t1)),"\n"); } my $index_id_sep = UR::Object::Index->__meta__->composite_id_separator() || "\t"; my %data_source_for_class = $self->get_data_sources_for_loaded_classes; # NOTE: This pokes right into the object cache and futzes with Index IDs directly. # We can't get the Index objects though get() because we'd recurse right back into here my %indexes_by_class; foreach my $idx_id ( keys %{$UR::Context::all_objects_loaded->{'UR::Object::Index'}} ) { my $class = substr($idx_id, 0, index($idx_id, $index_id_sep)); next unless exists $data_source_for_class{$class}; push @{$indexes_by_class{$class}}, $UR::Context::all_objects_loaded->{'UR::Object::Index'}->{$idx_id}; } my $deleted_count = 0; my $pass = 0; $cache_size_highwater = 1 if ($cache_size_highwater < 1); $cache_size_lowwater = 1 if ($cache_size_lowwater < 1); # Instead of sorting object cache by __get_serial, since we are trying to # conserve memory, we pass through the object cache reviewing chunks of older objects # first while working our way through the whole cache. my $target_serial = $cache_last_prune_serial; my $serial_range = ($GET_COUNTER - $target_serial); my $max_passes = 10; my $target_serial_increment = int($serial_range / $max_passes) + 1; while ($all_objects_cache_size > $cache_size_lowwater && $target_serial < $GET_COUNTER) { $pass++; $target_serial += $target_serial_increment; foreach my $class (keys %data_source_for_class) { my $objects_for_class = $UR::Context::all_objects_loaded->{$class}; $indexes_by_class{$class} ||= []; foreach my $id ( keys ( %$objects_for_class ) ) { my $obj = $objects_for_class->{$id}; next unless defined $obj; # object with this ID does not exist if ( $obj->is_weakened || $obj->is_prunable && $obj->{__get_serial} && $obj->{__get_serial} <= $target_serial ) { foreach my $index ( @{$indexes_by_class{$class}} ) { $index->weaken_reference_for_object($obj); } if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM PRUNE object $obj class $class id $id\n"; } delete $obj->{'__get_serial'}; Scalar::Util::weaken($objects_for_class->{$id}); $all_objects_cache_size--; $deleted_count++; } } } } $is_pruning = 0; $cache_last_prune_serial = $target_serial; if ($ENV{'UR_DEBUG_OBJECT_RELEASE'} || $ENV{'UR_DEBUG_OBJECT_PRUNING'}) { my $t2 = Time::HiRes::time(); printf("MEM PRUNE complete, $deleted_count objects marked after $pass passes in %.4f sec\n\n\n",$t2-$t1); } if ($all_objects_cache_size > $cache_size_lowwater) { Carp::carp "After several passes of pruning the object cache, there are still $all_objects_cache_size objects"; if ($ENV{'UR_DEBUG_OBJECT_PRUNING'}) { warn "Top 10 classes by object count:\n" . $self->_object_cache_pruning_report; } } return 1; } sub _object_cache_pruning_report { my $self = shift; my $max_show = shift; $max_show = 10 unless defined ($max_show); my @sorted_counts = sort { $b->[1] <=> $a->[1] } map { [ $_ => scalar(keys %{$UR::Context::all_objects_loaded->{$_}}) ] } grep { !$_->__meta__->is_meta_meta } keys %$UR::Context::all_objects_loaded; my $message = ''; for (my $i = 0; $i < 10 and $i < @sorted_counts; $i++) { my $class_name = $sorted_counts[$i]->[0]; my $count = $sorted_counts[$i]->[1]; $message .= "$class_name: $count\n"; if ($ENV{'UR_DEBUG_OBJECT_PRUNING'} > 1) { # more detailed info my $no_data_source = 0; my $other_references = 0; my $strengthened = 0; my $has_changes = 0; my $prunable = 0; my $class_data_source = eval { $class_name->__meta__->data_source_id; }; foreach my $obj ( values %{$UR::Context::all_objects_loaded->{$class_name}} ) { next unless $obj; my $is_prunable = 1; if (! $class_data_source ) { $no_data_source++; $is_prunable = 0; } if (! exists $obj->{'__get_serial'}) { $other_references++; $is_prunable = 0; } if (exists $obj->{'__strengthened'}) { $strengthened++; $is_prunable = 0; } if ($obj->__changes__) { $has_changes++; $is_prunable = 0; } if ($is_prunable) { $prunable++; } } $message .= sprintf("\tNo data source: %d other refs: %d strengthend: %d has changes: %d prunable: %d\n", $no_data_source, $other_references, $strengthened, $has_changes, $prunable); } } return $message; } sub value_for_object_property_in_underlying_context { my ($self, $obj, $property_name) = @_; my $saved = $obj->{db_saved_uncommitted} || $obj->{db_committed}; unless ($saved) { Carp::croak(qq(No object found in underlying context)); } return $saved->{$property_name}; } # True if the object was loaded from an underlying context and/or datasource, or if the # object has been committed to the underlying context sub object_exists_in_underlying_context { my($self, $obj) = @_; return if ($obj->{'__defined'}); return (exists($obj->{'db_committed'}) || exists($obj->{'db_saved_uncommitted'})); } # Holds the logic for handling OR-type rules passed to get_objects_for_class_and_rule() sub _get_objects_for_class_and_or_rule { my ($self, $class, $rule, $load, $return_closure) = @_; $rule = $rule->normalize; my @u = $rule->underlying_rules; my @results; for my $u (@u) { if (wantarray or not defined wantarray) { push @results, $self->get_objects_for_class_and_rule($class,$u,$load,$return_closure); } else { my $result = $self->get_objects_for_class_and_rule($class,$u,$load,$return_closure); push @results, $result; } } if ($return_closure) { my $object_sorter = $rule->template->sorter(); my @next; return sub { # fill in missing slots in @next for(my $i = 0; $i < @results; $i++) { unless (defined $next[$i]) { # This slot got used last time through $next[$i] = $results[$i]->(); unless (defined $next[$i]) { # That iterator is exhausted, splice it out splice(@results, $i, 1); splice(@next, $i, 1); redo if $i < @results; #the next iterator is now at $i, not $i++ } } } my $lowest_slot = 0; for(my $i = 1; $i < @results; $i++) { my $cmp = $object_sorter->($next[$lowest_slot], $next[$i]); if ($cmp > 0) { $lowest_slot = $i; } elsif ($cmp == 0) { # duplicate object, mark this slot to fill in next time around $next[$i] = undef; } } my $retval = $next[$lowest_slot]; $next[$lowest_slot] = undef; return $retval; }; } # remove duplicates my $last = 0; my $plast = 0; my $next = 0; @results = grep { $plast = $last; $last = $_; $plast == $_ ? () : ($_) } sort @results; return unless defined wantarray; return @results if wantarray; if (@results > 1) { $self->_exception_for_multi_objects_in_scalar_context($rule,\@results); } return $results[0]; } # this is the underlying method for get/load/is_loaded in ::Object sub get_objects_for_class_and_rule { my ($self, $class, $rule, $load, $return_closure) = @_; my $initial_load = $load; #my @params = $rule->params_list; #print "GET: $class @params\n"; my $rule_template = $rule->template; my $group_by = $rule_template->group_by; if (ref($self) and !defined($load)) { $load = $self->query_underlying_context; # could still be undef... } if ($group_by and $rule_template->order_by) { my %group_by = map { $_ => 1 } @{ $rule->template->group_by }; foreach my $order_by_property ( @{ $rule->template->order_by } ) { unless ($group_by{$order_by_property}) { Carp::croak("Property '$order_by_property' in the -order_by list must appear in the -group_by list for BoolExpr $rule"); } } } if ( $cache_size_highwater and $all_objects_cache_size > $cache_size_highwater ) { $self->prune_object_cache(); } if ($rule_template->isa("UR::BoolExpr::Template::Or")) { return $self->_get_objects_for_class_and_or_rule($class,$rule,$load,$return_closure); } # an identifier for all objects gotten in this request will be set/updated on each of them for pruning later my $this_get_serial = $GET_COUNTER++; my $meta = $class->__meta__(); # A query on a subclass where the parent class is_abstract and has a subclassify_by property # (meaning that the parent class has a property which directly stores the proper subclass for # each row - subclasses inherit the property from the parent, and the subclass isn't is_abstract) # should have a filter added to the rule to keep only rows of the subclass we're interested in. # This will improve the SQL performance when it's later constructed. my $subclassify_by = $meta->subclassify_by; if ($subclassify_by and ! $meta->is_abstract and ! $rule->template->group_by and ! $rule->specifies_value_for($subclassify_by) ) { $rule = $rule->add_filter($subclassify_by => $class); } # If $load is undefined, and there is no underlying context, we define it to FALSE explicitly # TODO: instead of checking for a data source, skip this # We'll always go to the underlying context, even if it has nothing. # This optimization only works by coincidence since we don't stack contexts currently beyond 1. my $ds; if (!defined($load) or $load) { ($ds) = $self->resolve_data_sources_for_class_meta_and_rule($meta,$rule); if (! $ds or $class =~ m/::Ghost$/) { # Classes without data sources and Ghosts can only ever come from the cache $load = 0; } } # this is an arrayref of all of the cached data # it is set in one of two places below my $cached; # this will turn foo=>$foo into foo.id=>$foo->id where possible my $no_hard_refs_rule = $rule->flatten_hard_refs; # we do not currently fully "flatten" b/c the bx constant_values do not flatten/reframe #my $flat_rule = ( (1 or $no_hard_refs_rule->subject_class_name eq 'UR::Object::Property') ? $no_hard_refs_rule : $no_hard_refs_rule->flatten); # this is a no-op if the rule is already normalized my $normalized_rule = $no_hard_refs_rule->normalize; my $is_monitor_query = $self->monitor_query; $self->_log_query_for_rule($class,$normalized_rule,Carp::shortmess("QUERY: Query start for rule $normalized_rule")) if ($is_monitor_query); # see if we need to load if load was not defined unless (defined $load) { # check to see if the cache is complete # also returns a list of the complete cached objects where that list is found as a side-effect my ($cache_is_complete, $cached) = $self->_cache_is_complete_for_class_and_normalized_rule($class, $normalized_rule); $load = ($cache_is_complete ? 0 : 1); } if ($ds and $load and $rule_template->order_by) { # if any of the order_by is calculated, then we need to do an unordered query against the # data source, then we can do it as a non-load query and do the sorting on all the in-memory # objects my $qp = $ds->_resolve_query_plan($rule_template); if ($qp->order_by_non_column_data) { $self->_log_query_for_rule($class,$normalized_rule,"QUERY: Doing an unordered query on the datasource because one of the order_by properties of the rule is not expressable by the data source") if ($is_monitor_query); $self->get_objects_for_class_and_rule($class, $rule->remove_filter('-order')->remove_filter('-order_by'), 1); $load = 0; } } my $normalized_rule_template = $normalized_rule->template; # optimization for the common case if (!$load and !$return_closure) { my @c = $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule); my $obj_count = scalar(@c); foreach ( @c ) { unless (exists $_->{'__get_serial'}) { # This is a weakened reference. Convert it back to a regular ref my $class = ref $_; my $id = $_->id; my $ref = $UR::Context::all_objects_loaded->{$class}->{$id}; $UR::Context::all_objects_loaded->{$class}->{$id} = $ref; } $_->{'__get_serial'} = $this_get_serial; } if ($is_monitor_query) { $self->_log_query_for_rule($class,$normalized_rule,"QUERY: matched $obj_count cached objects (no loading)"); $self->_log_query_for_rule($class,$normalized_rule,"QUERY: Query complete after returning $obj_count object(s) for rule $rule"); $self->_log_done_elapsed_time_for_rule($normalized_rule); } if (defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset(\@c,$normalized_rule_template); } return @c if wantarray; # array context return unless defined wantarray; # null context Carp::confess("multiple objects found for a call in scalar context!" . Data::Dumper::Dumper(\@c)) if @c > 1; return $c[0]; # scalar context } my $object_sorter = $normalized_rule_template->sorter(); # the above process might have found all of the cached data required as a side-effect in which case # we have a value for this early # either way: ensure the cached data is known and sorted if ($cached) { @$cached = sort $object_sorter @$cached; } else { $cached = [ sort $object_sorter $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule) ]; } $self->_log_query_for_rule($class, $normalized_rule, "QUERY: matched ".scalar(@$cached)." cached objects") if ($is_monitor_query); foreach ( @$cached ) { unless (exists $_->{'__get_serial'}) { # This is a weakened reference. Convert it back to a regular ref my $class = ref $_; my $id = $_->id; my $ref = $UR::Context::all_objects_loaded->{$class}->{$id}; $UR::Context::all_objects_loaded->{$class}->{$id} = $ref; } $_->{'__get_serial'} = $this_get_serial; } # make a loading iterator if loading must be done for this rule my $loading_iterator; if ($load) { # this returns objects from the underlying context after importing them into the current context, # but only if they did not exist in the current context already $self->_log_query_for_rule($class, $normalized_rule, "QUERY: importing from underlying context with rule $normalized_rule") if ($is_monitor_query); $loading_iterator = UR::Context::LoadingIterator->_create($cached, $self,$normalized_rule, $ds,$this_get_serial); } if ($return_closure) { if ($load) { # return the iterator made above return $loading_iterator; } else { # make a quick iterator for the cached data if(defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset($cached,$normalized_rule_template); } return sub { return shift @$cached }; } } else { my @results; if ($loading_iterator) { # use the iterator made above my $found; while (defined($found = $loading_iterator->(1))) { push @results, $found; } } else { # just get the cached data if(defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset($cached,$normalized_rule_template); } @results = @$cached; } return unless defined wantarray; return @results if wantarray; if (@results > 1) { $self->_exception_for_multi_objects_in_scalar_context($rule,\@results); } return $results[0]; } } sub _exception_for_multi_objects_in_scalar_context { my($self,$rule,$resultsref) = @_; my $message = sprintf("Multiple results unexpected for query.\n\tClass %s\n\trule params: %s\n\tGot %d results", $rule->subject_class_name, join(',', $rule->params_list), scalar(@$resultsref)); my $lastidx = $#$resultsref; if (@$resultsref > 10) { $message .= "; the first 10 are"; $lastidx = 9; } Carp::confess($message . ":\n" . Data::Dumper::Dumper([@$resultsref[0..$lastidx]])); } sub _prune_obj_list_for_limit_and_offset { my($self, $obj_list, $tmpl) = @_; my $limit = defined($tmpl->limit) ? $tmpl->limit : $#$obj_list; my $offset = $tmpl->offset || 0; if ($offset > @$obj_list) { Carp::carp('-offset is larger than the result list'); @$obj_list = (); } else { @$obj_list = splice(@$obj_list, $offset, $limit); } } sub __merge_db_data_with_existing_object { my($self, $class_name, $existing_object, $pending_db_object_data, $property_names) = @_; unless (defined $pending_db_object_data) { # This means a row in the database is missing for an object we loaded before if (defined($existing_object) and $self->object_exists_in_underlying_context($existing_object) and $existing_object->__changes__ ) { my $id = $existing_object->id; Carp::croak("$class_name ID '$id' previously existed in an underlying context, has since been deleted from that context, and the cached object now has unsavable changes.\nDump: ".Data::Dumper::Dumper($existing_object)."\n"); } else { #print "Removing object id ".$existing_object->id." because it has been removed from the database\n"; UR::Context::LoadingIterator->_remove_object_from_other_loading_iterators($existing_object); $existing_object->__signal_change__('delete'); $self->_abandon_object($existing_object); return $existing_object; } } my $expected_db_data; if (exists $existing_object->{'db_saved_uncommitted'}) { $expected_db_data = $existing_object->{'db_saved_uncommitted'}; } elsif (exists $existing_object->{'db_committed'}) { $expected_db_data = $existing_object->{'db_committed'}; } else { my $id = $existing_object->id; Carp::croak("$class_name ID '$id' has just been loaded, but it exists in the application as a new unsaved object!\nDump: " . Data::Dumper::Dumper($existing_object) . "\n"); } my $different = 0; my $conflict = undef; foreach my $property ( @$property_names ) { no warnings 'uninitialized'; # All direct properties are stored in the same-named hash key, right? next unless (exists $existing_object->{$property}); my $object_value = $existing_object->{$property}; my $db_value = $pending_db_object_data->{$property}; my $expected_db_value = $expected_db_data->{$property}; if ($object_value ne $expected_db_value) { $different++; } if ( $object_value eq $db_value # current value matches DB value or ($object_value eq $expected_db_value) # current value hasn't changed since it was loaded from the DB or ($db_value eq $expected_db_value) # DB value matches what it was when we loaded it from the DB ) { # no conflict. Check the next one next; } else { $conflict = $property; last; } } if (defined $conflict) { # conflicting change! # Since the user could be catching this exception, go ahead and update the # object's notion of what is in the database my %old_dbc = %$expected_db_data; @$expected_db_data{@$property_names} = @$pending_db_object_data{@$property_names}; my $old_value = defined($old_dbc{$conflict}) ? "'" . $old_dbc{$conflict} . "'" : '(undef)'; my $new_db_value = defined($pending_db_object_data->{$conflict}) ? "'" . $pending_db_object_data->{$conflict} . "'" : '(undef)'; my $new_obj_value = defined($existing_object->{$conflict}) ? "'" . $existing_object->{$conflict} . "'" : '(undef)'; my $obj_id = $existing_object->id; Carp::croak("\nA change has occurred in the database for $class_name property '$conflict' on object ID $obj_id from $old_value to $new_db_value.\n" . "At the same time, this application has made a change to that value to $new_obj_value.\n\n" . "The application should lock data which it will update and might be updated by other applications."); } # No conflicts. Update db_committed and db_saved_uncommitted based on the DB data %$expected_db_data = (%$expected_db_data, %$pending_db_object_data); if (! $different) { # FIXME HACK! This is to handle the case when you get an object, start a software transaction, # change something in the database for that object, reload the object (so __merge updates the value # found in the DB), then rollback the transaction. The act of updating the value here in __merge makes # a change record that gets undone when the transaction is rolled back. After the rollback, the current # value goes back to the originally loaded value, db_committed has the newly clhanged DB value, but # _change_count is 0 turning off change tracking makes it so this internal change isn't undone by rollback local $UR::Context::Transaction::log_all_changes = 0; # HACK! # The object has no local changes. Go ahead and update the current value, too foreach my $property ( @$property_names ) { no warnings 'uninitialized'; next if ($existing_object->{$property} eq $pending_db_object_data->{$property}); $existing_object->$property($pending_db_object_data->{$property}); } } # re-figure how many changes are really there my @change_count = $existing_object->__changes__; $existing_object->{'_change_count'} = scalar(@change_count); return $different; } sub _get_objects_for_class_and_sql { # this is a depracated back-door to get objects with raw sql # only use it if you know what you're doing my ($self, $class, $sql) = @_; my $meta = $class->__meta__; #my $ds = $self->resolve_data_sources_for_class_meta_and_rule($meta,$class->define_boolexpr()); my $ds = $self->resolve_data_sources_for_class_meta_and_rule($meta,UR::BoolExpr->resolve($class)); my $id_list = $ds->_resolve_ids_from_class_name_and_sql($class,$sql); return unless (defined($id_list) and @$id_list); my $rule = UR::BoolExpr->resolve_normalized($class, id => $id_list); return $self->get_objects_for_class_and_rule($class,$rule); } sub _cache_is_complete_for_class_and_normalized_rule { my ($self,$class,$normalized_rule) = @_; # TODO: convert this to use the rule object instead of going back to the legacy hash format my ($id,$params,@objects,$cache_is_complete); $params = $normalized_rule->legacy_params_hash; $id = $params->{id}; # Determine ahead of time whether we believe the object MUST be loaded if it exists. # If this is true, we will shortcut out of any action which loads or prepares for loading. # Try to resolve without loading in cases where we are sure # that doing so will return the complete results. my $id_only = $params->{_id_only}; $id_only = undef if ref($id) and ref($id) eq 'HASH'; if ($id_only) { # _id_only means that only id parameters were passed in. # Either a single id or an arrayref of ids. # Try to pull objects from the cache in either case if (ref $id) { # arrayref id # we check the immediate class and all derived # classes for any of the ids in the set. @objects = grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } ($class, $class->__meta__->subclasses_loaded); # see if we found all of the requested objects if (@objects == @$id) { # we found them all # return them all $cache_is_complete = 1; } else { # Ideally we'd filter out the ones we found, # but that gets complicated. # For now, we do it the slow way for partial matches @objects = (); } } else { # scalar id # Check for objects already loaded. no warnings; if (exists $all_objects_loaded->{$class}->{$id}) { $cache_is_complete = 1; @objects = grep { $_ } $all_objects_loaded->{$class}->{$id}; } elsif (not $class->isa("UR::Value")) { # we already checked the immediate class, # so just check derived classes # this is not done for values because an identity can exist # with independent objects with values, unlike entities @objects = grep { $_ } map { $all_objects_loaded->{$_}->{$id} } $class->__meta__->subclasses_loaded; if (@objects) { $cache_is_complete = 1; } } } } elsif ($params->{_unique}) { # _unique means that this set of params could never # result in more than 1 object. # See if the 1 is in the cache # If not we have to load @objects = $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule); if (@objects) { $cache_is_complete = 1; } } if ($cache_is_complete) { # if the $cache_is_comlete, the $cached list DEFINITELY represents all objects we need to return # we know that loading is NOT necessary because what we've found cached must be the entire set # Because we happen to have that set, we return it in addition to the boolean flag return wantarray ? (1, \@objects) : (); } # We need to do more checking to see if loading is necessary # Either the parameters were non-unique, or they were unique # and we didn't find the object checking the cache. # See if we need to do a load(): my $template_id = $normalized_rule->template_id; my $rule_id = $normalized_rule->id; my $loading_is_in_progress_on_another_iterator = grep { $_->is_loading_in_progress_for_boolexpr($normalized_rule) } UR::Context::ObjectFabricator->all_object_fabricators; return 0 if $loading_is_in_progress_on_another_iterator; # complex (non-single-id) params my $loading_was_done_before_with_these_params = ( # exact match to previous attempt ( exists ($UR::Context::all_params_loaded->{$template_id}) and exists ($UR::Context::all_params_loaded->{$template_id}->{$rule_id}) ) || # this is a subset of a previous attempt ($self->_loading_was_done_before_with_a_superset_of_this_rule($normalized_rule)) ); my $object_is_loaded_or_non_existent = $loading_was_done_before_with_these_params || $class->all_objects_are_loaded; if ($object_is_loaded_or_non_existent) { # These same non-unique parameters were used to load previously, # or we loaded everything at some point. # No load necessary. return 1; } else { # Load according to params return; } } # done setting $load, and possibly filling $cached/$cache_is_complete as a side-effect sub all_objects_loaded { my $self = shift; my $class = $_[0]; return( grep {$_} map { values %{ $UR::Context::all_objects_loaded->{$_} } } $class, $class->__meta__->subclasses_loaded ); } sub all_objects_loaded_unsubclassed { my $self = shift; my $class = $_[0]; return (grep {$_} values %{ $UR::Context::all_objects_loaded->{$class} } ); } sub _get_objects_for_class_and_rule_from_cache { # Get all objects which are loaded in the application which match # the specified parameters. my ($self, $class, $rule) = @_; my ($template,@values) = $rule->template_and_values; #my @param_list = $rule->params_list; #print "CACHE-GET: $class @param_list\n"; my $strategy = $rule->{_context_query_strategy}; unless ($strategy) { if ($rule->template->group_by) { $strategy = $rule->{_context_query_strategy} = "set intersection"; } elsif ($rule->num_values == 0) { $strategy = $rule->{_context_query_strategy} = "all"; } elsif ($rule->is_id_only) { $strategy = $rule->{_context_query_strategy} = "id"; } else { $strategy = $rule->{_context_query_strategy} = "index"; } } my @results = eval { if ($strategy eq "all") { return $self->all_objects_loaded($class); } elsif ($strategy eq "id") { my $id = $rule->value_for_id(); unless (defined $id) { $id = $rule->value_for_id(); } # Try to get the object(s) from this class directly with the ID. # Note that the code below is longer than it needs to be, but # is written to run quickly by resolving the most common cases # first, and gathering data only if and when it must. my @matches; if (ref($id) eq 'ARRAY') { # The $id is an arrayref. Get all of the set. @matches = grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } ($class); # We're done if the number found matches the number of ID values. return @matches if @matches == @$id; } else { # The $id is a normal scalar. if (not defined $id) { #Carp::carp("Undefined id passed as params for query on $class"); Carp::cluck("\n\n**** Undefined id passed as params for query on $class"); $id ||= ''; } my $match; # FIXME This is a performance optimization for class metadata to avoid the search through # @subclasses_loaded a few lines further down. When 100s of classes are loaded it gets # a bit slow. Maybe UR::Object::Type should override get() instad and put it there? if (! $UR::Object::Type::bootstrapping and $class eq 'UR::Object::Type') { my $meta_class_name = $id . '::Type'; $match = $all_objects_loaded->{$meta_class_name}->{$id} || $all_objects_loaded->{'UR::Object::Type'}->{$id}; if ($match) { return $match; } else { return; } } $match = $all_objects_loaded->{$class}->{$id}; # We're done if we found anything. If not we keep checking. return $match if $match; } # Try to get the object(s) from this class's subclasses. # We may be adding to matches made above is we used an arrayref # and the results are incomplete. my @subclasses_loaded = $class->__meta__->subclasses_loaded; return @matches unless @subclasses_loaded; if (ref($id) eq 'ARRAY') { # The $id is an arrayref. Get all of the set and add it to anything found above. push @matches, grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } @subclasses_loaded; } else { # The $id is a normal scalar, but we didn't find it above. # Try each subclass, exiting if we find anything. for (@subclasses_loaded) { my $match = $all_objects_loaded->{$_}->{$id}; return $match if $match; } } # Since an ID was specified, and we've scanned the core hash every way possible, # we're done. Return nothing if necessary. return @matches; } elsif ($strategy eq "index") { # FIXME - optimize by using the rule (template?)'s param names directly to get the # index id instead of re-figuring it out each time my $class_meta = $rule->subject_class_name->__meta__; my %params = $rule->params_list; my $should_evaluate_later; for my $key (keys %params) { if (substr($key,0,1) eq '-' or substr($key,0,1) eq '_') { delete $params{$key}; } elsif ($key =~ /^\w*\./) { # a chain of properties $should_evaluate_later = 1; delete $params{$key}; } else { my $prop_meta = $class_meta->property_meta_for_name($key); # NOTE: We _could_ remove the is_delegated check if we knew we were operating on # a read-only context. if ($prop_meta && ($prop_meta->is_many or $prop_meta->is_delegated)) { # These indexes perform poorly in the general case if we try to index # the is_many properties. Instead, strip them out from the basic param # list, and evaluate the superset of indexed objects through the rule $should_evaluate_later = 1; delete $params{$key}; } } } my @properties = sort keys %params; unless (@properties) { # All the supplied filters were is_many properties return grep { $rule->evaluate($_) } $self->all_objects_loaded($class); } my @values = map { $params{$_} } @properties; unless (@properties == @values) { Carp::confess(); } # find or create the index my $pstring = join(",",@properties); my $index_id = UR::Object::Index->__meta__->resolve_composite_id_from_ordered_values($class,$pstring); my $index = $all_objects_loaded->{'UR::Object::Index'}{$index_id}; $index ||= UR::Object::Index->create( id => $index_id, indexed_class_name => $class, indexed_property_string => $pstring ); # add the indexed objects to the results list if ($UR::Debug::verify_indexes) { my @matches = $index->get_objects_matching(@values); @matches = sort @matches; my @matches2 = sort grep { $rule->evaluate($_) } $self->all_objects_loaded($class); unless ("@matches" eq "@matches2") { print "@matches\n"; print "@matches2\n"; #Carp::cluck("Mismatch!"); my @matches3 = $index->get_objects_matching(@values); my @matches4 = $index->get_objects_matching(@values); return @matches2; } return @matches; } if ($should_evaluate_later) { return grep { $rule->evaluate($_) } $index->get_objects_matching(@values); } else { return $index->get_objects_matching(@values); } } elsif ($strategy eq 'set intersection') { #print $rule->num_values, " ", $rule->is_id_only, "\n"; my $template = $rule->template; my $group_by = $template->group_by; # get the objects in memory, and make sets for them if they do not exist my $rule_no_group = $rule->remove_filter('-group_by'); $rule_no_group = $rule_no_group->remove_filter('-order_by'); my @objects_in_set = $self->_get_objects_for_class_and_rule_from_cache($class, $rule_no_group); my @sets_from_grouped_objects = _group_objects($rule_no_group->template,\@values,$group_by,\@objects_in_set); # determine the template that the grouped subsets will use # find templates which are subsets of that template # find sets with a my $set_class = $class . '::Set'; my $expected_template_id = $rule->template->_template_for_grouped_subsets->id; my @matches = grep { # TODO: make the template something indexable so we can pull from index my $bx = UR::BoolExpr->get($_->id); my $bxt = $bx->template; if ($bxt->id ne $expected_template_id) { #print "TEMPLATE MISMATCH $expected_template_id does not match $bxt->{id}! set: $_ with bxid $bx->{id} cannot be under rule $rule_no_group" . Data::Dumper::Dumper($_); (); } elsif (not $bx->is_subset_of($rule_no_group) ) { #print "SUBSET MISMATCH: $rule_no_group is not a superset of $_ with bxid $bx->{id}" . Data::Dumper::Dumper($_); (); } else { #print "MATCH: $rule_no_group with $expected_template_id matches $bx $bx->{id}" . Data::Dumper::Dumper($_); ($_); } } $self->all_objects_loaded($set_class); # Code to check that newly fabricated set definitions are in the set we query back out: # my @all = $self->all_objects_loaded($set_class); # my %expected; # @expected{@sets_from_grouped_objects} = @sets_from_grouped_objects; # for my $match (@matches) { # delete $expected{$match}; # } # if (keys %expected) { # #$DB::single = 1; # print Data::Dumper::Dumper(\%expected); # } return @matches; } else { die "unknown strategy $strategy"; } }; # Handle passing-through any exceptions. die $@ if $@; if (my $recurse = $template->recursion_desc) { my ($this,$prior) = @$recurse; # remove undef items. undef/NULL in the recursion linkage means it doesn't link to anything my @values = grep { defined } map { $_->$prior } @results; if (@values) { # We do get here, so that adjustments to intermediate foreign keys # in the cache will result in a new query at the correct point, # and not result in missing data. #push @results, $class->get($this => \@values, -recurse => $recurse); push @results, map { $class->get($this => $_, -recurse => $recurse) } @values; } } my $group_by = $template->group_by; #if ($group_by) { # # return sets instead of the actual objects # @results = _group_objects($template,\@values,$group_by,\@results); #} if (@results > 1) { my $sorter; if ($group_by) { # We need to rewrite the original rule on the member class to be a rule # on the Set class to do proper ordering my $set_class = $template->subject_class_name . '::Set'; my $set_template = UR::BoolExpr::Template->resolve($set_class, -group_by => $group_by); $sorter = $set_template->sorter; } else { $sorter = $template->sorter; } @results = sort $sorter @results; } # Return in the standard way. return @results if (wantarray); Carp::confess("Multiple matches for $class @_!") if (@results > 1); return $results[0]; } sub _group_objects { my ($template,$values,$group_by,$objects) = @_; my $sub_template = $template->remove_filter('-group_by'); for my $property (@$group_by) { $sub_template = $sub_template->add_filter($property); } my $set_class = $template->subject_class_name . '::Set'; my @groups; my %seen; for my $result (@$objects) { my %values_for_group_property; foreach my $group_property ( @$group_by ) { my @values = $result->$group_property; if (@values) { $values_for_group_property{$group_property} = \@values; } else { $values_for_group_property{$group_property} = [ undef ]; } } my @combinations = UR::Util::combinations_of_values(map { $values_for_group_property{$_} } @$group_by); foreach my $extra_values ( @combinations ) { my $bx = $sub_template->get_rule_for_values(@$values,@$extra_values); next if $seen{$bx->id}++; my $group = $set_class->get($bx->id); push @groups, $group; } } return @groups; } sub _loading_was_done_before_with_a_superset_of_this_rule { my($self,$rule) = @_; my $template = $rule->template; if (exists $UR::Context::all_params_loaded->{$template->id} and exists $UR::Context::all_params_loaded->{$template->id}->{$rule->id} ) { return 1; } if ($template->subject_class_name->isa("UR::Value")) { return; } my @rule_values = $rule->values; my @rule_param_names = $template->_property_names; my %rule_values; for (my $i = 0; $i < @rule_param_names; $i++) { $rule_values{ $rule_param_names[$i] } = $rule_values[$i]; } foreach my $loaded_template_id ( keys %$UR::Context::all_params_loaded ) { my $loaded_template = UR::BoolExpr::Template->get($loaded_template_id); if($template->is_subset_of($loaded_template)) { # Try limiting the possibilities by matching the previously-loaded rule value_id's # on this rule's values my @param_names = $loaded_template->_property_names; my @values = @rule_values{ @param_names }; my $value_id; { no warnings 'uninitialized'; $value_id = join($UR::BoolExpr::Util::record_sep, @values); } my @candidates = grep { index($_, $value_id) > 0 } keys(%{ $UR::Context::all_params_loaded->{$loaded_template_id} }); foreach my $loaded_rule_id ( @candidates ) { my $loaded_rule = UR::BoolExpr->get($loaded_rule_id); return 1 if ($rule->is_subset_of($loaded_rule)); } } } return; } sub _forget_loading_was_done_with_template_and_rule { my($self,$template_id, $rule_id) = @_; delete $all_params_loaded->{$template_id}->{$rule_id}; } # Given a list of values, returns a list of lists containing all subsets of # the input list, including the original list and the empty list sub _get_all_subsets_of_params { my $self = shift; return [] unless @_; my $first = shift; my @rest = $self->_get_all_subsets_of_params(@_); return @rest, map { [$first, @$_ ] } @rest; } sub query_underlying_context { my $self = shift; unless (ref $self) { $self = $self->current; } if (@_) { $self->{'query_underlying_context'} = shift; } return $self->{'query_underlying_context'}; } # all of these delegate to the current context... sub has_changes { return shift->get_current->has_changes(@_); } sub commit { Carp::carp 'UR::Context::commit() called as a function, not a method. Assumming commit on current context' unless @_; my $self = shift; $self = UR::Context->current() unless ref $self; $self->__signal_change__('precommit'); unless ($self->_sync_databases) { $self->__signal_observers__('sync_databases', 0); $self->__signal_change__('commit',0); return; } $self->__signal_observers__('sync_databases', 1); unless ($self->_commit_databases) { $self->__signal_change__('commit',0); die "Application failure during commit!"; } $self->__signal_change__('commit',1); foreach ( $self->all_objects_loaded('UR::Object') ) { delete $_->{'_change_count'}; } return 1; } sub rollback { my $self = shift; unless ($self) { warn 'UR::Context::rollback() called as a function, not a method. Assumming rollback on current context'; $self = UR::Context->current(); } $self->__signal_change__('prerollback'); unless ($self->_reverse_all_changes) { $self->__signal_change__('rollback', 0); die "Application failure during reverse_all_changes?!"; } unless ($self->_rollback_databases) { $self->__signal_change__('rollback', 0); die "Application failure during rollback!"; } $self->__signal_change__('rollback', 1); return 1; } sub _tmp_self { my $self = shift; if (ref($self)) { return ($self,ref($self)); } else { return ($UR::Context::current, $self); } } sub clear_cache { my ($self,$class) = _tmp_self(shift @_); my %args = @_; # dont unload any of the infrastructional classes, or any classes # the user requested to be saved my %local_dont_unload; if ($args{'dont_unload'}) { for my $class_name (@{$args{'dont_unload'}}) { $local_dont_unload{$class_name} = 1; for my $subclass_name ($class_name->__meta__->subclasses_loaded) { $local_dont_unload{$subclass_name} = 1; } } } for my $class_name (UR::Object->__meta__->subclasses_loaded) { # Once transactions are fully implemented, the command params will sit # beneath the regular transaction, so we won't need this. For now, # we need a work-around. next if $class_name eq "UR::Command::Param"; next if $class_name->isa('UR::Singleton'); my $class_obj = $class_name->__meta__; #if ($class_obj->data_source and $class_obj->is_transactional) { # # normal #} #elsif (!$class_obj->data_source and !$class_obj->is_transactional) { # # expected #} #elsif ($class_obj->data_source and !$class_obj->is_transactional) { # Carp::confess("!!!!!data source on non-transactional class $class_name?"); #} #elsif (!$class_obj->data_source and $class_obj->is_transactional) { # # okay #} next unless $class_obj->is_uncachable; next if $class_obj->is_meta_meta; next unless $class_obj->is_transactional; next if ($local_dont_unload{$class_name} || grep { $class_name->isa($_) } @{$args{'dont_unload'}}); next if $class_obj->is_meta; next if not defined $class_obj->data_source; for my $obj ($self->all_objects_loaded_unsubclassed($class_name)) { # Check the type against %local_dont_unload again, because all_objects_loaded() # will return child class objects, as well as the class you asked for. For example, # GSC::DNA->a_o_l() will also return GSC::ReadExp objects, and the user may have wanted # to save those. We also check whether the $obj type isa one of the requested classes # because, for example, GSC::Sequence->a_o_l returns GSC::ReadExp types, and the user # may have wanted to save all GSC::DNAs my $obj_type = ref $obj; next if ($local_dont_unload{$obj_type} || grep {$obj_type->isa($_) } @{$args{'dont_unload'}}); $obj->unload; } my @obj = grep { defined($_) } values %{ $UR::Context::all_objects_loaded->{$class_name} }; if (@obj) { $class->warning_message("Skipped unload of $class_name objects during clear_cache: " . join(",",map { $_->id } @obj ) . "\n" ); if (my @changed = grep { $_->__changes__ } @obj) { require YAML; $class->error_message( "The following objects have changes:\n" . Data::Dumper::Dumper(\@changed) . "The clear_cache method cannot be called with unsaved changes on objects.\n" . "Use reverse_all_changes() first to really undo everything, then clear_cache()," . " or call sync_database() and clear_cache() if you want to just lighten memory but keep your changes.\n" . "Clearing the cache with active changes will be supported after we're sure all code like this is gone. :)\n" ); exit 1; } } delete $UR::Context::all_objects_loaded->{$class_name}; delete $UR::Context::all_objects_are_loaded->{$class_name}; delete $UR::Context::all_params_loaded->{$class_name}; } 1; } sub _order_data_sources_for_saving { my @data_sources = @_; my %can_savepoint = map { $_->id => $_->can_savepoint } @data_sources; my %classes = map { $_->id => $_->class } @data_sources; my %is_default = map { $_->id => $_->isa('UR::DataSource::Default') ? -1 : 0 } @data_sources; # Default data sources go last return sort { $is_default{$a->id} <=> $is_default{$b->id} || $can_savepoint{$a->id} <=> $can_savepoint{$b->id} || $classes{$a->id} cmp $classes{$b->id} } @data_sources; } our $IS_SYNCING_DATABASE = 0; sub _sync_databases { my $self = shift; my %params = @_; # Glue App::DB->sync_database with UR::Context->_sync_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API # You'll also want to remove all the gotos from this function and uncomment # the returns return 1 if $IS_SYNCING_DATABASE; $IS_SYNCING_DATABASE = 1; if ($App::DB::{'sync_database'}) { unless (App::DB->sync_database() ) { $IS_SYNCING_DATABASE = 0; $self->error_message(App::DB->error_message()); return; } } $IS_SYNCING_DATABASE = 0; # This should be far down enough to avoid recursion, right? my @o = grep { ref($_) eq 'UR::DeletedRef' } $self->all_objects_loaded('UR::Object'); if (@o) { print Data::Dumper::Dumper(\@o); Carp::confess(); } # Determine what has changed. my @changed_objects = ( $self->all_objects_loaded('UR::Object::Ghost'), grep { $_->__changes__ } $self->all_objects_loaded('UR::Object') #UR::Util->mapreduce_grep(sub { $_[0]->__changes__ },$self->all_objects_loaded('UR::Object')) ); return 1 unless (@changed_objects); # Ensure validity. # This is primarily to catch custom validity logic in class overrides. my @invalid = grep { $_->__errors__ } @changed_objects; #my @invalid = UR::Util->mapreduce_grep(sub { $_[0]->__errors__}, @changed_objects); if (@invalid) { $self->display_invalid_data_for_save(\@invalid); goto PROBLEM_SAVING; #return; } # group changed objects by data source my %ds_objects; for my $obj (@changed_objects) { my $data_source = $self->resolve_data_source_for_object($obj); next unless $data_source; my $data_source_id = $data_source->id; $ds_objects{$data_source_id} ||= { 'ds_obj' => $data_source, 'changed_objects' => []}; push @{ $ds_objects{$data_source_id}->{'changed_objects'} }, $obj; } my @ds_in_order = map { $_->id } _order_data_sources_for_saving(map { $_->{ds_obj} } values(%ds_objects)); # save on each in succession my @done; my $rollback_on_non_savepoint_handle; for my $data_source_id (@ds_in_order) { my $obj_list = $ds_objects{$data_source_id}->{'changed_objects'}; my $data_source = $ds_objects{$data_source_id}->{'ds_obj'}; my $result = $data_source->_sync_database( %params, changed_objects => $obj_list, ); if ($result) { push @done, $data_source; next; } else { $self->error_message( "Failed to sync data source: $data_source_id: " . $data_source->error_message ); for my $prev_data_source (@done) { $prev_data_source->_reverse_sync_database; } goto PROBLEM_SAVING; #return; } } return 1; PROBLEM_SAVING: if ($App::DB::{'rollback'}) { App::DB->rollback(); } return; } sub display_invalid_data_for_save { my $self = shift; my @objects_with_errors = @{shift @_}; $self->error_message('Invalid data for save!'); for my $obj (@objects_with_errors) { no warnings; my $identifier = eval { $obj->__display_name__ } || $obj->id; my $msg = $obj->class . " identified by " . $identifier . " has problems on\n"; my @problems = $obj->__errors__; foreach my $error ( @problems ) { $msg .= $error->__display_name__ . "\n"; } $msg .= " Current state:\n"; my $datadumper = Data::Dumper::Dumper($obj); my $nr_of_lines = $datadumper =~ tr/\n//; if ($nr_of_lines > 40) { # trim it down to the first 15 and last 3 lines $datadumper =~ m/^((?:.*\n){15})/; $msg .= $1; $datadumper =~ m/((?:.*(?:\n|$)){3})$/; $msg .= "[...]\n$1\n"; } else { $msg .= $datadumper; } $self->error_message($msg); } return 1; } sub _reverse_all_changes { my $self = shift; my $class; if (ref($self)) { $class = ref($self); } else { $class = $self; $self = $UR::Context::current; } @UR::Context::Transaction::open_transaction_stack = (); @UR::Context::Transaction::change_log = (); $UR::Context::Transaction::log_all_changes = 0; $UR::Context::current = $UR::Context::process; my @objects = map { $self->all_objects_loaded_unsubclassed($_) } grep { $_->__meta__->is_transactional } grep { ! $_->isa('UR::Value') } sort UR::Object->__meta__->subclasses_loaded(); for my $object (@objects) { $object->__rollback__(); } return 1; } our $IS_COMMITTING_DATABASE = 0; sub _commit_databases { my $class = shift; # Glue App::DB->commit() with UR::Context->_commit_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API return 1 if $IS_COMMITTING_DATABASE; $IS_COMMITTING_DATABASE = 1; if ($App::DB::{'commit'}) { unless (App::DB->commit() ) { $IS_COMMITTING_DATABASE = 0; $class->error_message(App::DB->error_message()); return; } } $IS_COMMITTING_DATABASE = 0; my @ds_in_order = _order_data_sources_for_saving($UR::Context::current->all_objects_loaded('UR::DataSource')); my @committed; foreach my $ds ( @ds_in_order ) { if ($ds->commit) { push @committed, $ds; } else { my $message = 'Data source ' . $ds->get_name . ' failed to commit: ' . join("\n\t", $ds->error_messages); if (@committed) { $message .= "\nThese data sources were successfully committed, resulting in a FRAGMENTED DISTRIBUTED TRANSACTION: " . join(', ', map { $_->get_name } @committed); } Carp::croak($message); } } return 1; } our $IS_ROLLINGBACK_DATABASE = 0; sub _rollback_databases { my $class = shift; # Glue App::DB->rollback() with UR::Context->_rollback_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API return 1 if $IS_ROLLINGBACK_DATABASE; $IS_ROLLINGBACK_DATABASE = 1; if ($App::DB::{'rollback'}) { unless (App::DB->rollback()) { $IS_ROLLINGBACK_DATABASE = 0; $class->error_message(App::DB->error_message()); return; } } $IS_ROLLINGBACK_DATABASE = 0; $class->_for_each_data_source("rollback") or die "FAILED TO ROLLBACK!: " . $class->error_message; return 1; } sub _disconnect_databases { my $class = shift; $class->_for_each_data_source("disconnect"); return 1; } sub _for_each_data_source { my($class,$method) = @_; my @ds = $UR::Context::current->all_objects_loaded('UR::DataSource'); foreach my $ds ( @ds ) { unless ($ds->$method) { $class->error_message("$method failed on DataSource ",$ds->get_name); return; } } return 1; } sub _get_committed_property_value { my $class = shift; my $object = shift; my $property_name = shift; if ($object->{'db_committed'}) { return $object->{'db_committed'}->{$property_name}; } elsif ($object->{'db_saved_uncommitted'}) { return $object->{'db_saved_uncommitted'}->{$property_name}; } else { return; } } sub _dump_change_snapshot { my $class = shift; my %params = @_; my @c = grep { $_->__changes__ } $UR::Context::current->all_objects_loaded('UR::Object'); my $fh; if (my $filename = $params{filename}) { $fh = IO::File->new(">$filename"); unless ($fh) { $class->error_message("Failed to open file $filename: $!"); return; } } else { $fh = "STDOUT"; } require YAML; $fh->print(YAML::Dump(\@c)); $fh->close; } sub reload { my $self = shift; # this is here for backward external compatability # get() now goes directly to the context my $class = shift; if (ref $class) { # Trying to reload a specific object? if (@_) { Carp::confess("load() on an instance with parameters is not supported"); return; } @_ = ('id' ,$class->id()); $class = ref $class; } my ($rule, @extra) = UR::BoolExpr->resolve_normalized($class,@_); if (@extra) { if (scalar @extra == 2 and ($extra[0] eq "sql" or $extra[0] eq 'sql in')) { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } else { die "Odd parameters passed directly to $class load(): @extra.\n" . "Processable params were: " . Data::Dumper::Dumper({ $rule->params_list }); } } return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } ## This is old, untested code that we may wany to resurrect at some point # #our $CORE_DUMP_VERSION = 1; ## Use Data::Dumper to save a representation of the object cache to a file. Args are: ## filename => the name of the file to save to ## dumpall => boolean flagging whether to dump _everything_, or just the things ## that would actually be loaded later in core_restore() # #sub _core_dump { # my $class = shift; # my %args = @_; # # my $filename = $args{'filename'} || "/tmp/core." . UR::Context::Process->prog_name . ".$ENV{HOST}.$$"; # my $dumpall = $args{'dumpall'}; # # my $fh = IO::File->new(">$filename"); # if (!$fh) { # $class->error_message("Can't open dump file $filename for writing: $!"); # return undef; # } # # my $dumper; # if ($dumpall) { # Go ahead and dump everything # $dumper = Data::Dumper->new([$CORE_DUMP_VERSION, # $UR::Context::all_objects_loaded, # $UR::Context::all_objects_are_loaded, # $UR::Context::all_params_loaded, # $UR::Context::all_change_subscriptions], # ['dump_version','all_objects_loaded','all_objects_are_loaded', # 'all_params_loaded','all_change_subscriptions']); # } else { # my %DONT_UNLOAD = # map { # my $co = $_->__meta__; # if ($co and not $co->is_transactional) { # ($_ => 1) # } # else { # () # } # } # $UR::Context::current->all_objects_loaded('UR::Object'); # # my %aol = map { ($_ => $UR::Context::all_objects_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_objects_loaded; # my %aoal = map { ($_ => $UR::Context::all_objects_are_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_objects_are_loaded; # my %apl = map { ($_ => $UR::Context::all_params_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_params_loaded; # # don't dump $UR::Context::all_change_subscriptions # $dumper = Data::Dumper->new([$CORE_DUMP_VERSION,\%aol, \%aoal, \%apl], # ['dump_version','all_objects_loaded','all_objects_are_loaded', # 'all_params_loaded']); # # } # # $dumper->Purity(1); # For dumping self-referential data structures # $dumper->Sortkeys(1); # Makes quick and dirty file comparisons with sum/diff work correctly-ish # # $fh->print($dumper->Dump() . "\n"); # # $fh->close; # # return $filename; #} # ## Read a file previously generated with core_dump() and repopulate the object cache. Args are: ## filename => name of the coredump file ## force => boolean flag whether to go ahead and attempt to load the file even if it thinks ## there is a formatting problem #sub _core_restore { # my $class = shift; # my %args = @_; # my $filename = $args{'filename'}; # my $forcerestore = $args{'force'}; # # my $fh = IO::File->new("$filename"); # if (!$fh) { # $class->error_message("Can't open dump file $filename for restoring: $!"); # return undef; # } # # my $code; # while (<$fh>) { $code .= $_ } # # my($dump_version,$all_objects_loaded,$all_objects_are_loaded,$all_params_loaded,$all_change_subscriptions); # eval $code; # # if ($@) # { # $class->error_message("Failed to restore core file state: $@"); # return undef; # } # if ($dump_version != $CORE_DUMP_VERSION) { # $class->error_message("core file's version $dump_version differs from expected $CORE_DUMP_VERSION"); # return 0 unless $forcerestore; # } # # my %DONT_UNLOAD = # map { # my $co = $_->__meta__; # if ($co and not $co->is_transactional) { # ($_ => 1) # } # else { # () # } # } # $UR::Context::current->all_objects_loaded('UR::Object'); # # # Go through the loaded all_objects_loaded, prune out the things that # # are in %DONT_UNLOAD # my %loaded_classes; # foreach ( keys %$all_objects_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_objects_loaded->{$_} = $all_objects_loaded->{$_}; # $loaded_classes{$_} = 1; # # } # foreach ( keys %$all_objects_are_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_objects_are_loaded->{$_} = $all_objects_are_loaded->{$_}; # $loaded_classes{$_} = 1; # } # foreach ( keys %$all_params_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_params_loaded->{$_} = $all_params_loaded->{$_}; # $loaded_classes{$_} = 1; # } # # $UR::Context::all_change_subscriptions is basically a bunch of coderef # # callbacks that can't reliably be dumped anyway, so we skip it # # # Now, get the classes to instantiate themselves # foreach ( keys %loaded_classes ) { # $_->class() unless m/::Ghost$/; # } # # return 1; #} 1; =pod =head1 NAME UR::Context - Manage the current state of the application =head1 SYNOPSIS use AppNamespace; my $obj = AppNamespace::SomeClass->get(id => 1234); $obj->some_property('I am changed'); UR::Context->get_current->rollback; # some_property reverts to its original value $obj->other_property('Now, I am changed'); UR::Context->commit; # other_property now permanently has that value =head1 DESCRIPTION The main application code will rarely interact with UR::Context objects directly, except for the C and C methods. It manages the mappings between an application's classes, object cache, and external data sources. =head1 SUBCLASSES UR::Context is an abstract class. When an application starts up, the system creates a handful of Contexts that logically exist within one another: =over 2 =item 1. L - A context to represent all the data reachable in the application's namespace. It connects the application to external data sources. =item 2. L - A context to represent the state of data within the currently running application. It handles the transfer of data to and from the Root context, through the object cache, on behalf of the application code. =item 3. L - A context to represent an in-memory transaction as a diff of the object cache. The Transaction keeps a list of changes to objects and is able to revert those changes with C, or apply them to the underlying context with C. =back =head1 CONSTRUCTOR =over 4 =item begin my $trans = UR::Context::Transaction->begin(); L instances are created through C. =back A L and L context will be created for you when the application initializes. Additional instances of these classes are not usually instantiated. =head1 METHODS Most of the methods below can be called as either a class or object method of UR::Context. If called as a class method, they will operate on the current context. =over 4 =item get_current my $context = UR::Context::get_current(); Returns the UR::Context instance of whatever is the most currently created Context. Can be called as a class or object method. =item query_underlying_context my $should_load = $context->query_underlying_context(); $context->query_underlying_context(1); A property of the Context that sets the default value of the C<$should_load> flag inside C as described below. Initially, its value is undef, meaning that during a get(), the Context will query the underlying data sources only if this query has not been done before. Setting this property to 0 will make the Context never query data sources, meaning that the only objects retrievable are those already in memory. Setting the property to 1 means that every query will hit the data sources, even if the query has been done before. =item get_objects_for_class_and_rule @objs = $context->get_objects_for_class_and_rule( $class_name, $boolexpr, $should_load, $should_return_iterator ); This is the method that serves as the main entry point to the Context behind the C, and C methods of L, and C method of UR::Context. C<$class_name> and C<$boolexpr> are required arguments, and specify the target class by name and the rule used to filter the objects the caller is interested in. C<$should_load> is a flag indicating whether the Context should load objects satisfying the rule from external data sources. A true value means it should always ask the relevent data sources, even if the Context believes the requested data is in the object cache, A false but defined value means the Context should not ask the data sources for new data, but only return what is currently in the cache matching the rule. The value C means the Context should use the value of its query_underlying_context property. If that is also undef, then it will use its own judgement about asking the data sources for new data, and will merge cached and external data as necessary to fulfill the request. C<$should_return_iterator> is a flag indicating whether this method should return the objects directly as a list, or iterator function instead. If true, it returns a subref that returns one object each time it is called, and undef after the last matching object: my $iter = $context->get_objects_for_class_and_rule( 'MyClass', $rule, undef, 1 ); my @objs; while (my $obj = $iter->()); push @objs, $obj; } =item has_changes my $bool = $context->has_changes(); Returns true if any objects in the given Context's object cache (or the current Context if called as a class method) have any changes that haven't been saved to the underlying context. =item commit UR::Context->commit(); Causes all objects with changes to save their changes back to the underlying context. If the current context is a L, then the changes will be applied to whatever Context the transaction is a part of. if the current context is a L context, then C pushes the changes to the underlying L context, meaning that those changes will be applied to the relevent data sources. In the usual case, where no transactions are in play and all data sources are RDBMS databases, calling C will cause the program to begin issuing SQL against the databases to update changed objects, insert rows for newly created objects, and delete rows from deleted objects as part of an SQL transaction. If all the changes apply cleanly, it will do and SQL C, or C if not. commit() returns true if all the changes have been safely transferred to the underlying context, false if there were problems. =item rollback UR::Context->rollback(); Causes all objects' changes for the current transaction to be reversed. If the current context is a L, then the transactional properties of those objects will be reverted to the values they had when the transaction started. Outside of a transaction, object properties will be reverted to their values when they were loaded from the underlying data source. rollback() will also ask all the underlying databases to rollback. =item clear_cache UR::Context->clear_cache(); Asks the current context to remove all non-infrastructional data from its object cache. This method will fail and return false if any object has changes. =item resolve_data_source_for_object my $ds = $obj->resolve_data_source_for_object(); For the given C<$obj> object, return the L instance that object was loaded from or would be saved to. If objects of that class do not have a data source, then it will return C. =item resolve_data_sources_for_class_meta_and_rule my @ds = $context->resolve_data_sources_for_class_meta_and_rule($class_obj, $boolexpr); For the given class metaobject and boolean expression (rule), return the list of data sources that will need to be queried in order to return the objects matching the rule. In most cases, only one data source will be returned. =item infer_property_value_from_rule my $value = $context->infer_property_value_from_rule($property_name, $boolexpr); For the given boolean expression (rule), and a property name not mentioned in the rule, but is a property of the class the rule is against, return the value that property must logically have. For example, if this object is the only TestClass object where C is the value 'bar', it can infer that the TestClass property C must have the value 'blah' in the current context. my $obj = TestClass->create(id => 1, foo => 'bar', baz=> 'blah'); my $rule = UR::BoolExpr->resolve('TestClass', foo => 'bar); my $val = $context->infer_property_value_from_rule('baz', $rule); # val now is 'blah' =item object_cache_size_highwater UR::Context->object_cache_size_highwater(5000); my $highwater = UR::Context->object_cache_size_highwater(); Set or get the value for the Context's object cache pruning high water mark. The object cache pruner will be run during the next C if the cache contains more than this number of prunable objects. See the L section below for more information. =item object_cache_size_lowwater UR::Context->object_cache_size_lowwater(5000); my $lowwater = UR::Context->object_cache_size_lowwater(); Set or get the value for the Context's object cache pruning high water mark. The object cache pruner will stop when the number of prunable objects falls below this number. =item prune_object_cache UR::Context->prune_object_cache(); Manually run the object cache pruner. =item reload UR::Context->reload($object); UR::Context->reload('Some::Class', 'property_name', value); Ask the context to load an object's data from an underlying Context, even if the object is already cached. With a single parameter, it will use that object's ID parameters as the basis for querying the data source. C will also accept a class name and list of key/value parameters the same as C. =item _light_cache UR::Context->_light_cache(1); Turn on or off the light caching flag. Light caching alters the behavior of the object cache in that all object references in the cache are made weak by Scalar::Util::weaken(). This means that the application code must keep hold of any object references it wants to keep alive. Light caching defaults to being off, and must be explicitly turned on with this method. =back =head1 Custom observer aspects UR::Context sends signals for observers watching for some non-standard aspects. =over 2 =item precommit After C has been called, but before any changes are saved to the data sources. The only parameters to the Observer's callback are the Context object and the aspect ("precommit"). =item commit After C has been called, and after an attempt has been made to save the changes to the data sources. The parameters to the callback are the Context object, the aspect ("commit"), and a boolean value indicating whether the commit succeeded or not. =item prerollback After C has been called, but before and object state is reverted. =item rollback After C has been called, and after an attempt has been made to revert the state of all the loaded objects. The parameters to the callback are the Context object, the aspect ("rollback"), and a boolean value indicating whether the rollback succeeded or not. =back =head1 Data Concurrency Currently, the Context is optimistic about data concurrency, meaning that it does very little to prevent clobbering data in underlying Contexts during a commit() if other processes have changed an object's data after the Context has cached and object. For example, a database has an object with ID 1 and a property with value 'bob'. A program loads this object and changes the property to 'fred', but does not yet commit(). Meanwhile, another program loads the same object, changes the value to 'joe' and does commit(). Finally the first program calls commit(). The final value in the database will be 'fred', and no exceptions will be raised. As part of the caching behavior, the Context keeps a record of what the object's state is as it's loaded from the underlying Context. This is how the Context knows what object have been changed during C. If an already cached object's data is reloaded as part of some other query, data consistency of each property will be checked. If there are no conflicting changes, then any differences between the object's initial state and the current state in the underlying Context will be applied to the object's notion of what it thinks its initial state is. In some future release, UR may support additional data concurrency methods such as pessimistic concurrency: check that the current state of all changed (or even all cached) objects in the underlying Context matches the initial state before committing changes downstream. Or allowing the object cache to operate in write-through mode for some or all classes. =head1 Internal Methods There are many methods in UR::Context meant to be used internally, but are worth documenting for anyone interested in the inner workings of the Context code. =over 4 =item _create_import_iterator_for_underlying_context $subref = $context->_create_import_iterator_for_underlying_context( $boolexpr, $data_source, $serial_number ); $next_obj = $subref->(); This method is part of the object loading process, and is called by L when it is determined that the requested data does not exist in the object cache, and data should be brought in from another, underlying Context. Usually this means the data will be loaded from an external data source. C<$boolexpr> is the L rule, usually from the application code. C<$data_source> is the L that will be used to load data from. C<$serial_number> is used by the object cache pruner. Each object loaded through this iterator will have $serial_number in its C<__get_serial> hashref key. It works by first getting an iterator for the data source (the C<$db_iterator>). It calls L to find out how data is to be loaded and whether this request spans multiple data sources. It calls L to get a list of closures to transform the primary data source's data into UR objects, and L (if necessary) to get more closures that can load and join data from the primary to the secondary data source(s). It returns a subref that works as an iterator, loading and returning objects one at a time from the underlying context into the current context. It returns undef when there are no more objects to return. The returned iterator works by first asking the C<$db_iterator> for the next row of data as a listref. Asks the secondary data source joiners whether there is any matching data. Calls the object fabricator closures to convert the data source data into UR objects. If any of the object requires subclassing, then additional importing iterators are created to handle that. Finally, the objects matching the rule are returned to the caller one at a time. =item _resolve_query_plan_for_ds_and_bxt my $query_plan = $context->_resolve_query_plan_for_ds_and_bxt( $data_source, $boolexpr_tmpl ); my($query_plan, @addl_info) = $context->_resolve_query_plan_for_ds_and_bxt( $data_source, $boolexpr_tmpl ); When a request is made that will hit one or more data sources, C<_resolve_query_plan_for_ds_and_bxt> is used to call a method of the same name on the data source. It retuns a hashref used by many other parts of the object loading system, and describes what data source to use, how to query that data source to get the objects, how to use the raw data returned by the data source to construct objects and how to resolve any delegated properties that are a part of the rule. C<$data_source> is a L object ID. C<$coolexpr_tmpl> is a L object. In the common case, the query will only use one data source, and this method returns that data directly. But if the primary data source sets the C key on the data structure as may be the case when a rule involves a delegated property to a class that uses a different data source, then this methods returns an additional list of data. For each additional data source needed to resolve the query, this list will have three items: =over 2 =item 1. The secondary data source ID =item 2. A listref of delegated L objects joining the primary data source to this secondary data source. =item 3. A L rule template applicable against the secondary data source =back =item _create_secondary_rule_from_primary my $new_rule = $context->_create_secondary_rule_from_primary( $primary_rule, $delegated_properties, $secondary_rule_tmpl ); When resolving a request that requires multiple data sources, this method is used to construct a rule against applicable against the secondary data source. C<$primary_rule> is the L rule used in the original query. C<$delegated_properties> is a listref of L objects as returned by L linking the primary to the secondary data source. C<$secondary_rule_tmpl> is the rule template, also as returned by L. =item _create_secondary_loading_closures my($obj_importers, $joiners) = $context->_create_secondary_loading_closures( $primary_rule_tmpl, @addl_info); When reolving a request that spans multiple data sources, this method is used to construct two lists of subrefs to aid in the request. C<$primary_rule_tmpl> is the L rule template made from the original rule. C<@addl_info> is the same list returned by L. For each secondary data source, there will be one item in the two listrefs that are returned, and in the same order. C<$obj_importers> is a listref of subrefs used as object importers. They transform the raw data returned by the data sources into UR objects. C<$joiners> is also a listref of subrefs. These closures know how the properties link the primary data source data to the secondary data source. They take the raw data from the primary data source, load the next row of data from the secondary data source, and returns the secondary data that successfully joins to the primary data. You can think of these closures as performing the same work as an SQL C between data in different data sources. =item _cache_is_complete_for_class_and_normalized_rule ($is_cache_complete, $objects_listref) = $context->_cache_is_complete_for_class_and_normalized_rule( $class_name, $boolexpr ); This method is part of the object loading process, and is called by L to determine if the objects requested by the L C<$boolexpr> will be found entirely in the object cache. If the answer is yes then C<$is_cache_complete> will be true. C<$objects_listef> may or may not contain objects matching the rule from the cache. If that list is not returned, then L does additional work to locate the matching objects itself via L It does its magic by looking at the C<$boolexpr> and loosely matching it against the query cache C<$UR::Context::all_params_loaded> =item _get_objects_for_class_and_rule_from_cache @objects = $context->_get_objects_for_class_and_rule_from_cache( $class_name, $boolexpr ); This method is called by L when L<_cache_is_complete_for_class_and_normalized_rule> says the requested objects do exist in the cache, but did not return those items directly. The L C<$boolexpr> contains hints about how the matching data is likely to be found. Its C<_context_query_strategy> key will contain one of three values =over 2 =item 1. all This rule is against a class with no filters, meaning it should return every member of that class. It calls C<$class-Eall_objects_loaded> to extract all objects of that class in the object cache. =item 2. id This rule is against a class and filters by only a single ID, or a list of IDs. The request is fulfilled by plucking the matching objects right out of the object cache. =item 3. index This rule is against one more more non-id properties. An index is built mapping the filtered properties and their values, and the cached objects which have those values. The request is fulfilled by using the index to find objects matching the filter. =item 4. set intersection This is a group-by rule and will return a ::Set object. =back =item _loading_was_done_before_with_a_superset_of_this_params_hashref $bool = $context->_loading_was_done_before_with_a_superset_of_this_params_hashref( $class_name, $params_hashref ); This method is used by L to determine if the requested data was asked for previously, either from a get() asking for a superset of the current request, or from a request on a parent class of the current request. For example, if a get() is done on a class with one param: @objs = ParentClass->get(param_1 => 'foo'); And then later, another request is done with an additional param: @objs2 = ParentClass->get(param_1 => 'foo', param_2 => 'bar'); Then the first request must have returned all the data that could have possibly satisfied the second request, and so the system will not issue a query against the data source. As another example, given those two previously done queries, if another get() is done on a class that inherits from ParentClass @objs3 = ChildClass->get(param_1 => 'foo'); again, the first request has already loaded all the relevent data, and therefore won't query the data source. =item _sync_databases $bool = $context->_sync_databases(); Starts the process of committing all the Context's changes to the external data sources. _sync_databases() is the workhorse behind L. First, it finds all objects with changes. Checks those changed objects for validity with C<$obj-Einvalid>. If any objects are found invalid, then _sync_databases() will fail. Finally, it bins all the changed objects by data source, and asks each data source to save those objects' changes. It returns true if all the data sources were able to save the changes, false otherwise. =item _reverse_all_changes $bool = $context->_reverse_all_changes(); _reverse_all_changes() is the workhorse behind L. For each class, it goes through each object of that class. If the object is a L, representing a deleted object, it converts the ghost back to the live version of the object. For other classes, it makes a list of properties that have changed since they were loaded (represented by the C hash key in the object), and reverts those changes by using each property's accessor method. =back =head1 The Object Cache The object cache is integral to the way the Context works, and also the main difference between UR and other ORMs. Other systems do no caching and require the calling application to hold references to any objects it is interested in. Say one part of the app loads data from the database and gives up its references, then if another part of the app does the same or similar query, it will have to ask the database again. UR handles caching of classes, objects and queries to avoid asking the data sources for data it has loaded previously. The object cache is essentially a software transaction that sits above whatever database transaction is active. After objects are loaded, any changes, creations or deletions exist only in the object cache, and are not saved to the underlying data sources until the application explicitly requests a commit or rollback. Objects are returned to the application only after they are inserted into the object cache. This means that if disconnected parts of the application are returned objects with the same class and ID, they will have references to the same exact object reference, and changes made in one part will be visible to all other parts of the app. An unchanged object can be removed from the object cache by calling its C method. Since changes to the underlying data sources are effectively delayed, it is possible that the application's notion of the object's current state does not match the data stored in the data source. You can mitigate this by using the C class or object method to fetch the latest data if it's a problem. Another issue to be aware of is if multiple programs are likely to commit conflicting changes to the same data, then whichever applies its changes last will win; some kind of external locking needs to be applied. Finally, if two programs attempt to insert data with the same ID columns into an RDBMS table, the second application's commit will fail, since that will likely violate a constraint. =head2 Object Change Tracking As objects are loaded from their data sources, their properties are initialized with the data from the query, and a copy of the same data is stored in the object in its C hash key. Anyone can ask the object for a list of its changes by calling C<$obj-Echanged>. Internally, changed() goes through all the object's properties, comparing the current values in the object's hash with the same keys under 'db_committed'. Objects created through the C class method have no 'db_committed', and so the object knows it it a newly created object in this context. Every time an object is retrieved with get() or through an iterator, it is assigned a serial number in its C<__get_serial> hash key from the C<$UR::Context::GET_SERIAL> counter. This number is unique and increases with each get(), and is used by the L to expire the least recently requested data. Objects also track what parameters have been used to get() them in the hash C<$obj-E{__load}>. This is a copy of the data in C<$UR::Context::all_params_loaded-E{$template_id}>. For each rule ID, it will have a count of the number of times that rule was used in a get(). =head2 Deleted Objects and Ghosts Calling delete() on an object is tracked in a different way. First, a new object is created, called a ghost. Ghost classes exist for every class in the application and are subclasses of L. For example, the ghost class for MyClass is MyClass::Ghost. This ghost object is initialized with the data from the original object. The original object is removed from the object cache, and is reblessed into the UR::DeletedRef class. Any attempt to interact with the object further will raise an exception. Ghost objects are not included in a get() request on the regular class, though the app can ask for them specificly using Cget(%params)>. Ghost classes do not have ghost classes themselves. Calling create() or delete() on a Ghost class or object will raise an exception. Calling other methods on the Ghost object that exist on the original, live class will delegate over to the live class's method. =head2 all_objects_are_loaded C<$UR::Context::all_objects_are_loaded> is a hashref keyed by class names. If the value is true, then L knows that all the instances of that class exist in the object cache, and it can avoid asking the underlying context/datasource for that class' data. =head2 all_params_loaded C<$UR::Context::all_params_loaded> is a two-level hashref. The first level is class names. The second level is rule (L) IDs. The values are how many times that class and rule have been involved in a get(). This data is used by L to determine if the requested data will be found in the object cache for non-id queries. =head2 all_objects_loaded C<$UR::Context::all_objects_loaded> is a two-level hashref. The first level is class names. The second level is object IDs. Every time an object is created, defined or loaded from an underlying context, it is inserted into the C hash. For queries involving only ID properties, the Context can retrieve them directly out of the cache if they appear there. The entire cache can be purged of non-infrastructional objects by calling L. =head2 Object Cache Pruner The default Context behavior is to cache all objects it knows about for the entire life of the process. For programs that churn through large amounts of data, or live for a long time, this is probably not what you want. The Context has two settings to loosely control the size of the object cache. L and L. As objects are created and loaded, a count of uncachable objects is kept in C<$UR::Context::all_objects_cache_size>. The first part of L checks to see of the current size is greater than the highwater setting, and call L if so. prune_object_cache() works by looking at what C<$UR::Context::GET_SERIAL> was the last time it ran, and what it is now, and making a guess about what object serial number to use as a guide for removing objects by starting at 10% of the difference between the last serial and the current value, called the target serial. It then starts executing a loop as long as C<$UR::Context::all_objects_cache_size> is greater than the lowwater setting. For each uncachable object, if its C<__get_serial> is less than the target serial, it is weakened from any Les it may be a member of, and then weakened from the main object cache, C<$UR::Context::all_objects_loaded>. The application may lock an object in the cache by calling C<__strengthen__> on it, Likewise, the app may hint to the pruner to throw away an object as soon as possible by calling C<__weaken__>. =head1 SEE ALSO L, L, L, L, L, L =cut AutoUnloadPool.pm100664023532023421 727112544604516 17627 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::AutoUnloadPool; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION use Scalar::Util qw(); # These are plain Perl objects that get garbage collected in the normal way, # not UR::Objects our @CARP_NOT = qw( UR::Context ); sub create { my $class = shift; my $self = bless { pool => {} }, $class; $self->_attach_observer(); return $self; } sub delete { my $self = shift; delete $self->{pool}; $self->_detach_observer(); } sub _attach_observer { my $self = shift; Scalar::Util::weaken($self); my $o = UR::Object->add_observer( aspect => 'load', callback => sub { my $loaded = shift; return if ! $loaded->is_prunable(); $self->_object_was_loaded($loaded); } ); $self->{observer} = $o; } sub _detach_observer { my $self = shift; delete($self->{observer})->delete(); } sub _is_printing_debug { $ENV{UR_DEBUG_OBJECT_PRUNING} || $ENV{'UR_DEBUG_OBJECT_RELEASE'}; } sub _object_was_loaded { my($self, $o) = @_; if (_is_printing_debug()) { my($class, $id) = ($o->class, $o->id); print STDERR Carp::shortmess("MEM AUTORELEASE $class id $id loaded in pool $self\n"); } $self->{pool}->{$o->class}->{$o->id} = undef; } sub _unload_objects { my $self = shift; return unless $self->{pool}; print STDERR Carp::shortmess("MEM AUTORELEASE pool $self draining\n") if _is_printing_debug(); my @unload_exceptions; foreach my $class_name ( keys %{$self->{pool}} ) { print STDERR "MEM AUTORELEASE class $class_name: " if _is_printing_debug(); my $is_subsequent_obj; my $objs_for_class = $UR::Context::all_objects_loaded->{$class_name}; next unless $objs_for_class; foreach ( @$objs_for_class{ keys %{$self->{pool}->{$class_name}}} ) { next unless $_; print STDERR ($is_subsequent_obj++ ? ", " : ''), $_->id,"\n" if _is_printing_debug(); unless (eval { $_->unload(); 1; } ) { push @unload_exceptions, $@; } } print STDERR "\n" if _is_printing_debug(); } delete $self->{pool}; die join("\n", 'The following exceptions happened while unloading:', @unload_exceptions) if @unload_exceptions; } sub DESTROY { local $@; my $self = shift; return unless ($self->{pool}); $self->_detach_observer(); $self->_unload_objects(); } 1; =pod =head1 NAME UR::Context::AutoUnloadPool - Automaticaly unload objects when scope ends =head1 SYNOPSIS my $not_unloaded = Some::Class->get(...); do { my $guard = UR::Context::AutoUnloadPool->create(); my $object = Some::Class->get(...); # load an object from the database ... # load more things }; # $guard goes out of scope - unloads objects =head1 DESCRIPTION UR Objects retrieved from the database normally live in the object cache for the life of the program. When a UR::Context::AutoUnloadPool is instantiated, it tracks every object loaded during its life. The Pool's destructor calls unload() on those objects. Changed objects and objects loaded before before the Pool is created will not get unloaded. =head1 METHODS =over 4 =item create my $guard = UR::Context::AutoUnloadPool->create(); Creates a Pool object. All UR Objects loaded from the database during this object's lifetime will get unloaded when the Pool goes out of scope. =item delete $guard->delete(); Invalidates the Pool object. No objects are unloaded. When the Pool later goes out of scope, no objects will be unloaded. =back =head1 SEE ALSO UR::Object, UR::Context =cut DefaultRoot.pm100664023532023421 64412544604516 17127 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::DefaultRoot; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Context::DefaultRoot', is => ['UR::Context::Root'], doc => 'The base context used when no special base context is specified.', ); 1; =pod =head1 NAME UR::Context::DefaultRoot - The base context used when no special base context is specified =cut ImportIterator.pm100664023532023421 12265612544604516 17753 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context; # Methods related to the import iterator (part of the loading process). # # They are broken out here for readability purposes. The methods still live # in the UR::Context namespace. use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; # A wrapper around the method of the same name in UR::DataSource::* to iterate over the # possible data sources involved in a query. The easy case (a query against a single data source) # will return the $primary_template data structure. If the query involves more than one data source, # then this method also returns a list containing triples (@addl_loading_info) where each member is: # 1) The secondary data source name # 2) a listref of delegated properties joining the primary class to the secondary class # 3) a rule template applicable against the secondary data source sub _resolve_query_plan_for_ds_and_bxt { my($self,$primary_data_source,$rule_template) = @_; my $primary_query_plan = $primary_data_source->_resolve_query_plan($rule_template); unless ($primary_query_plan->{'joins_across_data_sources'}) { # Common, easy case return $primary_query_plan; } my @addl_loading_info; foreach my $secondary_data_source_id ( keys %{$primary_query_plan->{'joins_across_data_sources'}} ) { my $this_ds_delegations = $primary_query_plan->{'joins_across_data_sources'}->{$secondary_data_source_id}; my %seen_properties; foreach my $delegated_property ( @$this_ds_delegations ) { my $delegated_property_name = $delegated_property->property_name; next if ($seen_properties{$delegated_property_name}++); my $operator = $rule_template->operator_for($delegated_property_name); $operator ||= '='; # FIXME - shouldn't the template return this for us? my @secondary_params = ($delegated_property->to . ' ' . $operator); my $class_meta = UR::Object::Type->get($delegated_property->class_name); my $relation_property = $class_meta->property_meta_for_name($delegated_property->via); my $secondary_class = $relation_property->data_type; # we can also add in any properties in the property's joins that also appear in the rule my @property_pairs = $relation_property->get_property_name_pairs_for_join(); foreach my $pair ( @property_pairs ) { my($primary_property, $secondary_property) = @$pair; next if ($seen_properties{$primary_property}++); next unless ($rule_template->specifies_value_for($primary_property)); my $operator = $rule_template->operator_for($primary_property); $operator ||= '='; push @secondary_params, "$secondary_property $operator"; } my $secondary_rule_template = UR::BoolExpr::Template->resolve($secondary_class, @secondary_params); # FIXME there should be a way to collect all the requests for the same datasource together... # FIXME - currently in the process of switching to object-based instead of class-based data sources # For now, data sources are still singleton objects, so this get() will work. When we're fully on # regular-object-based data sources, then it'll probably change to UR::DataSource->get($secondary_data_source_id); my $secondary_data_source = UR::DataSource->get($secondary_data_source_id) || $secondary_data_source_id->get(); push @addl_loading_info, $secondary_data_source, [$delegated_property], $secondary_rule_template; } } return ($primary_query_plan, @addl_loading_info); } # Used by _create_secondary_loading_comparators to convert a rule against the primary data source # to a rule that can be used against a secondary data source # FIXME this might be made simpler be leaning on infer_property_value_from_rule()? sub _create_secondary_rule_from_primary { my($self,$primary_rule, $delegated_properties, $secondary_rule_template) = @_; my @secondary_values; my %seen_properties; # FIXME - we've already been over this list in _resolve_query_plan_for_ds_and_bxt()... # FIXME - is there ever a case where @$delegated_properties will be more than one item? foreach my $property ( @$delegated_properties ) { my $value = $primary_rule->value_for($property->property_name); my $secondary_property_name = $property->to; my $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name); $secondary_values[$pos] = $value; $seen_properties{$property->property_name}++; my $class_meta = $property->class_meta; my $via_property = $class_meta->property_meta_for_name($property->via); my @pairs = $via_property->get_property_name_pairs_for_join(); foreach my $pair ( @pairs ) { my($primary_property_name, $secondary_property_name) = @$pair; next if ($seen_properties{$primary_property_name}++); $value = $primary_rule->value_for($primary_property_name); next unless $value; $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name); $secondary_values[$pos] = $value; } } my $secondary_rule = $secondary_rule_template->get_rule_for_values(@secondary_values); return $secondary_rule; } # Since we'll be appending more "columns" of data to the listrefs returned by # the primary datasource's query, we need to apply fixups to the column positions # to all the secondary loading templates # The column_position and object_num offsets needed for the next call of this method # are returned sub _fixup_secondary_loading_template_column_positions { my($self,$primary_loading_templates, $secondary_loading_templates, $column_position_offset, $object_num_offset) = @_; if (! defined($column_position_offset) or ! defined($object_num_offset)) { $column_position_offset = 0; foreach my $tmpl ( @{$primary_loading_templates} ) { $column_position_offset += scalar(@{$tmpl->{'column_positions'}}); } $object_num_offset = scalar(@{$primary_loading_templates}); } my $this_template_column_count; foreach my $tmpl ( @$secondary_loading_templates ) { foreach ( @{$tmpl->{'column_positions'}} ) { $_ += $column_position_offset; } foreach ( @{$tmpl->{'id_column_positions'}} ) { $_ += $column_position_offset; } $tmpl->{'object_num'} += $object_num_offset; $this_template_column_count += scalar(@{$tmpl->{'column_positions'}}); } return ($column_position_offset + $this_template_column_count, $object_num_offset + scalar(@$secondary_loading_templates) ); } # For queries that have to hit multiple data sources, this method creates two lists of # closures. The first is a list of object fabricators, where the loading templates # have been given fixups to the column positions (see _fixup_secondary_loading_template_column_positions()) # The second is a list of closures for each data source (the @addl_loading_info stuff # from _resolve_query_plan_for_ds_and_bxt) that's able to compare the row loaded from the # primary data source and see if it joins to a row from this secondary datasource's database sub _create_secondary_loading_closures { my($self, $primary_template, $rule, @addl_loading_info) = @_; my $loading_templates = $primary_template->{'loading_templates'}; # Make a mapping of property name to column positions returned by the primary query my %primary_query_column_positions; foreach my $tmpl ( @$loading_templates ) { my $property_name_count = scalar(@{$tmpl->{'property_names'}}); for (my $i = 0; $i < $property_name_count; $i++) { my $property_name = $tmpl->{'property_names'}->[$i]; my $pos = $tmpl->{'column_positions'}->[$i]; $primary_query_column_positions{$property_name} = $pos; } } my @secondary_object_importers; my @addl_join_comparators; # used to shift the apparent column position of the secondary loading template info my ($column_position_offset,$object_num_offset); while (@addl_loading_info) { my $secondary_data_source = shift @addl_loading_info; my $this_ds_delegations = shift @addl_loading_info; my $secondary_rule_template = shift @addl_loading_info; my $secondary_rule = $self->_create_secondary_rule_from_primary ( $rule, $this_ds_delegations, $secondary_rule_template, ); $secondary_data_source = $secondary_data_source->resolve_data_sources_for_rule($secondary_rule); my $secondary_template = $self->_resolve_query_plan_for_ds_and_bxt($secondary_data_source,$secondary_rule_template); # sets of triples where the first in the triple is the column index in the # $secondary_db_row (in the join_comparator closure below), the second is the # index in the $next_db_row. And the last is a flag indicating if we should # perform a numeric comparison. This way we can preserve the order the comparisons # should be done in my @join_comparison_info; foreach my $property ( @$this_ds_delegations ) { # first, map column names in the joined class to column names in the primary class my %foreign_property_name_map; my @this_property_joins = $property->_resolve_join_chain(); foreach my $join ( @this_property_joins ) { my @source_names = @{$join->{'source_property_names'}}; my @foreign_names = @{$join->{'foreign_property_names'}}; @foreign_property_name_map{@foreign_names} = @source_names; } # Now, find out which numbered column in the result query maps to those names my $secondary_loading_templates = $secondary_template->{'loading_templates'}; foreach my $tmpl ( @$secondary_loading_templates ) { my $property_name_count = scalar(@{$tmpl->{'property_names'}}); for (my $i = 0; $i < $property_name_count; $i++) { my $property_name = $tmpl->{'property_names'}->[$i]; if ($foreign_property_name_map{$property_name}) { # This is the one we're interested in... Where does it come from in the primary query? my $column_position = $tmpl->{'column_positions'}->[$i]; # What are the types involved? my $primary_query_column_name = $foreign_property_name_map{$property_name}; my $primary_property_meta = UR::Object::Property->get(class_name => $primary_template->{'class_name'}, property_name => $primary_query_column_name); my $secondary_property_meta = UR::Object::Property->get(class_name => $secondary_template->{'class_name'}, property_name => $property_name); my $comparison_type; if ($primary_property_meta->is_numeric && $secondary_property_meta->is_numeric) { $comparison_type = 1; } my $comparison_position; if (exists $primary_query_column_positions{$primary_query_column_name} ) { $comparison_position = $primary_query_column_positions{$primary_query_column_name}; } else { # This isn't a real column we can get from the data source. Maybe it's # in the constant_property_names of the primary_loading_template? unless (grep { $_ eq $primary_query_column_name} @{$loading_templates->[0]->{'constant_property_names'}}) { die sprintf("Can't resolve datasource comparison to join %s::%s to %s:%s", $primary_template->{'class_name'}, $primary_query_column_name, $secondary_template->{'class_name'}, $property_name); } my $comparison_value = $rule->value_for($primary_query_column_name); unless (defined $comparison_value) { $comparison_value = $self->infer_property_value_from_rule($primary_query_column_name, $rule); } $comparison_position = \$comparison_value; } push @join_comparison_info, $column_position, $comparison_position, $comparison_type; } } } } my $secondary_db_iterator = $secondary_data_source->create_iterator_closure_for_rule($secondary_rule); my $secondary_db_row; # For this closure, pass in the row we just loaded from the primary DB query. # This one will return the data from this secondary DB's row if the passed-in # row successfully joins to this secondary db iterator. It returns an empty list # if there were no matches, and returns false if there is no more data from the query my $join_comparator = sub { my $next_db_row = shift; # From the primary DB READ_DB_ROW: while(1) { return unless ($secondary_db_iterator); unless ($secondary_db_row) { ($secondary_db_row) = $secondary_db_iterator->(); unless($secondary_db_row) { # No more data to load $secondary_db_iterator = undef; return; } } for (my $i = 0; $i < @join_comparison_info; $i += 3) { my $secondary_column = $join_comparison_info[$i]; my $primary_column = $join_comparison_info[$i+1]; my $is_numeric = $join_comparison_info[$i+2]; my $comparison; if (ref $primary_column) { # This was one of those constant value items if ($is_numeric) { $comparison = $secondary_db_row->[$secondary_column] <=> $$primary_column; } else { $comparison = $secondary_db_row->[$secondary_column] cmp $$primary_column; } } else { if ($join_comparison_info[$i+2]) { $comparison = $secondary_db_row->[$secondary_column] <=> $next_db_row->[$primary_column]; } else { $comparison = $secondary_db_row->[$secondary_column] cmp $next_db_row->[$primary_column]; } } if ($comparison < 0) { # less than, get the next row from the secondary DB $secondary_db_row = undef; redo READ_DB_ROW; } elsif ($comparison == 0) { # This one was the same, keep looking at the others } else { # greater-than, there's no match for this primary DB row return 0; } } # All the joined columns compared equal, return the data return $secondary_db_row; } }; Sub::Name::subname('UR::Context::__join_comparator(closure)__', $join_comparator); push @addl_join_comparators, $join_comparator; # And for the object importer/fabricator, here's where we need to shift the column order numbers # over, because these closures will be called after all the db iterators' rows are concatenated # together. We also need to make a copy of the loading_templates list so as to not mess up the # class' notion of where the columns are # FIXME - it seems wasteful that we need to re-created this each time. Look into some way of using # the original copy that lives in $primary_template->{'loading_templates'}? Somewhere else? my @secondary_loading_templates; foreach my $tmpl ( @{$secondary_template->{'loading_templates'}} ) { my %copy; foreach my $key ( keys %$tmpl ) { my $value_to_copy = $tmpl->{$key}; if (ref($value_to_copy) eq 'ARRAY') { $copy{$key} = [ @$value_to_copy ]; } elsif (ref($value_to_copy) eq 'HASH') { $copy{$key} = { %$value_to_copy }; } else { $copy{$key} = $value_to_copy; } } push @secondary_loading_templates, \%copy; } ($column_position_offset,$object_num_offset) = $self->_fixup_secondary_loading_template_column_positions($primary_template->{'loading_templates'}, \@secondary_loading_templates, $column_position_offset,$object_num_offset); #my($secondary_rule_template,@secondary_values) = $secondary_rule->get_template_and_values(); my @secondary_values = $secondary_rule->values(); foreach my $secondary_loading_template ( @secondary_loading_templates ) { my $secondary_object_importer = UR::Context::ObjectFabricator->create_for_loading_template( $self, $secondary_loading_template, $secondary_template, $secondary_rule, $secondary_rule_template, \@secondary_values, $secondary_data_source ); next unless $secondary_object_importer; push @secondary_object_importers, $secondary_object_importer; } } return (\@secondary_object_importers, \@addl_join_comparators); } # This returns an iterator that is used to bring objects in from an underlying # context into this context. sub _create_import_iterator_for_underlying_context { my ($self, $rule, $dsx, $this_get_serial) = @_; # TODO: instead of taking a data source, resolve this internally. # The underlying context itself should be responsible for its data sources. # Make an iterator for the primary data source. # Primary here meaning the one for the class we're explicitly requesting. # We may need to join to other data sources to complete the query. my ($db_iterator) = $dsx->create_iterator_closure_for_rule($rule); my ($rule_template, @values) = $rule->template_and_values(); my ($query_plan,@addl_loading_info) = $self->_resolve_query_plan_for_ds_and_bxt($dsx,$rule_template); my $class_name = $query_plan->{class_name}; my $group_by = $rule_template->group_by; my $order_by = $rule_template->order_by; my $aggregate = $rule_template->aggregate; my $limit = $rule_template->limit; if (my $sub_typing_property) { # When the rule has a property specified which indicates a specific sub-type, catch this and re-call # this method recursively with the specific subclass name. my ($rule_template, @values) = $rule->template_and_values(); my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype}; my $class_table_name = $query_plan->{class_table_name}; warn "Implement me carefully"; if ($rule_template_specifies_value_for_subtype) { my $sub_classification_meta_class_name = $query_plan->{sub_classification_meta_class_name}; my $value = $rule->value_for($sub_typing_property); my $type_obj = $sub_classification_meta_class_name->get($value); if ($type_obj) { my $subclass_name = $type_obj->subclass_name($class_name); if ($subclass_name and $subclass_name ne $class_name) { #$rule = $subclass_name->define_boolexpr($rule->params_list, $sub_typing_property => $value); $rule = UR::BoolExpr->resolve_normalized($subclass_name, $rule->params_list, $sub_typing_property => $value); return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial); } } else { die "No $value for $class_name?\n"; } } elsif (not $class_table_name) { die "No longer supported!"; my $rule = UR::BoolExpr->resolve( $class_name, $rule_template->get_rule_for_values(@values)->params_list, ); return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial) } else { # continue normally # the logic below will handle sub-classifying each returned entity } } my $loading_templates = $query_plan->{loading_templates}; my $sub_typing_property = $query_plan->{sub_typing_property}; my $next_db_row; my $rows = 0; # number of rows the query returned my $recursion_desc = $query_plan->{recursion_desc}; my($rule_template_without_recursion_desc, $rule_template_id_without_recursion); my($rule_without_recursion_desc, $rule_id_without_recursion); # These get set if you're doing a -recurse query, and the underlying data source doesn't support recursion my($by_hand_recursive_rule_template,$by_hand_recursive_source_property,@by_hand_recursive_source_values,$by_hand_recursing_iterator); if ($recursion_desc) { $rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc}; $rule_template_id_without_recursion = $rule_template_without_recursion_desc->id; $rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values); $rule_id_without_recursion = $rule_without_recursion_desc->id; if ($query_plan->{'recurse_resolution_by_iteration'}) { # The data source does not support a recursive query. Accomplish the same thing by # recursing back into _create_import_iterator_for_underlying_context for each level my $this; ($this,$by_hand_recursive_source_property) = @$recursion_desc; my @extra; $by_hand_recursive_rule_template = UR::BoolExpr::Template->resolve($class_name, "$this in"); $by_hand_recursive_rule_template->recursion_desc($recursion_desc); if (!$by_hand_recursive_rule_template or @extra) { Carp::croak("Can't resolve recursive query: Class $class_name cannot filter by one or more properties: " . join(', ', @extra)); } } } my $rule_id = $rule->id; my $rule_template_id = $rule_template->id; my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'}; my %subordinate_iterator_for_class; # TODO: move the creation of the fabricators into the query plan object initializer. # instead of making just one import iterator, we make one per loading template # we then have our primary iterator use these to fabricate objects for each db row my @object_fabricators; if ($group_by) { # returning sets for each sub-group instead of instance objects... my $division_point = scalar(@$group_by)-1; my $subset_template = $rule_template->_template_for_grouped_subsets(); my $set_class = $class_name . '::Set'; my @aggregate_properties = ($aggregate ? @$aggregate : ()); unshift(@aggregate_properties, 'count') unless (grep { $_ eq 'count' } @aggregate_properties); my $fab_subref = sub { my $row = $_[0]; my @group_values = @$row[0..$division_point]; my $ss_rule = $subset_template->get_rule_for_values(@values, @group_values); my $set = $set_class->get($ss_rule->id); unless ($set) { Carp::croak("Failed to fabricate $set_class for rule $ss_rule"); } my $aggregates = $set->{__aggregates} ||= {}; @$aggregates{@aggregate_properties} = @$row[$division_point+1..$#$row]; return $set; }; my $object_fabricator = UR::Context::ObjectFabricator->_create( fabricator => $fab_subref, context => $self, ); unshift @object_fabricators, $object_fabricator; } else { # regular instances for my $loading_template (@$loading_templates) { my $object_fabricator = UR::Context::ObjectFabricator->create_for_loading_template( $self, $loading_template, $query_plan, $rule, $rule_template, \@values, $dsx, ); next unless $object_fabricator; unshift @object_fabricators, $object_fabricator; } } # For joins across data sources, we need to create importers/fabricators for those # classes, as well as callbacks used to perform the equivalent of an SQL join in # UR-space my @addl_join_comparators; if (@addl_loading_info) { if ($group_by) { Carp::croak("cross-datasource group-by is not supported yet"); } my($addl_object_fabricators, $addl_join_comparators) = $self->_create_secondary_loading_closures( $query_plan, $rule, @addl_loading_info ); unshift @object_fabricators, @$addl_object_fabricators; push @addl_join_comparators, @$addl_join_comparators; } # To avoid calling the useless method 'fabricate' on a fabricator object for each object of each resultset row my @object_fabricator_closures = map { $_->fabricator } @object_fabricators; # Insert the key into all_objects_are_loaded to indicate that when we're done loading, we'll # have everything if ($query_plan->{'rule_matches_all'} and not $group_by) { $class_name->all_objects_are_loaded(undef); } #my $is_monitor_query = $self->monitor_query(); # Make the iterator we'll return. my $next_object_to_return; my @object_ids_from_fabricators; my $underlying_context_iterator = sub { return undef unless $db_iterator; my $primary_object_for_next_db_row; LOAD_AN_OBJECT: until (defined $primary_object_for_next_db_row) { # note that we return directly when the db is out of data my ($next_db_row); ($next_db_row) = $db_iterator->() if ($db_iterator); if (! $next_db_row and $by_hand_recursive_rule_template and @by_hand_recursive_source_values) { # DB is out of results for this query, we need to handle recursion here in the context # and there are values to recurse on unless ($by_hand_recursing_iterator) { # Do a new get() on the data source to recursively get more data my $recurse_rule = $by_hand_recursive_rule_template->get_rule_for_values(\@by_hand_recursive_source_values); $by_hand_recursing_iterator = $self->_create_import_iterator_for_underlying_context($recurse_rule,$dsx,$this_get_serial); } my $retval = $next_object_to_return; $next_object_to_return = $by_hand_recursing_iterator->(); unless ($next_object_to_return) { $by_hand_recursing_iterator = undef; $by_hand_recursive_rule_template = undef; } return $retval; } unless ($next_db_row) { $db_iterator = undef; if ($rows == 0) { # if we got no data at all from the sql then we give a status # message about it and we update all_params_loaded to indicate # that this set of parameters yielded 0 objects my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only}; if ($rule_template_is_id_only) { my $id = $rule->value_for_id; $UR::Context::all_objects_loaded->{$class_name}->{$id} = undef; } else { $UR::Context::all_params_loaded->{$rule_template_id}->{$rule_id} = 0; } } if ( $query_plan->{rule_matches_all} ) { # No parameters. We loaded the whole class. # Doing a load w/o a specific ID w/o custom SQL loads the whole class. # Set a flag so that certain optimizations can be made, such as # short-circuiting future loads of this class. # # If the key still exists in the all_objects_are_loaded hash, then # we can set it to true. This is needed in the case where the user # gets an iterator for all the objects of some class, but unloads # one or more of the instances (be calling unload or through the # cache pruner) before the iterator completes. If so, _abandon_object() # will have removed the key from the hash if (exists($UR::Context::all_objects_are_loaded->{$class_name})) { $class_name->all_objects_are_loaded(1); } } if ($recursion_desc) { my @results = $class_name->is_loaded($rule_without_recursion_desc); $UR::Context::all_params_loaded->{$rule_template_id_without_recursion}{$rule_id_without_recursion} = scalar(@results); for my $object (@results) { $object->{__load}->{$rule_template_id_without_recursion}->{$rule_id_without_recursion}++; } } # Apply changes to all_params_loaded that each importer has collected foreach (@object_fabricators) { $_->finalize if $_; } # If the SQL for the subclassed items was constructed properly, then each # of these iterators should be at the end, too. Call them one more time # so they'll finalize their object fabricators. foreach my $class ( keys %subordinate_iterator_for_class ) { my $obj = $subordinate_iterator_for_class{$class}->(); if ($obj) { # The last time this happened, it was because a get() was done on an abstract # base class with only 'id' as a param. When the subclassified rule was # turned into SQL in UR::DataSource::QueryPlan() # it removed that one 'id' filter, since it assummed any class with more than # one ID property (usually classes have a named whatever_id property, and an alias 'id' # property) will have a rule that covered both ID properties Carp::carp("Leftover objects in subordinate iterator for $class. This shouldn't happen, but it's not fatal..."); while ($obj = $subordinate_iterator_for_class{$class}->()) {1;} } } my $retval = $next_object_to_return; $next_object_to_return = undef; return $retval; } # we count rows processed mainly for more concise sanity checking $rows++; # For multi-datasource queries, does this row successfully join with all the other datasources? # # Normally, the policy is for the data source query to return (possibly) more than what you # asked for, and then we'd cache everything that may have been loaded. In this case, we're # making the choice not to. Reason being that a join across databases is likely to involve # a lot of objects, and we don't want to be stuffing our object cache with a lot of things # we're not interested in. FIXME - in order for this to be true, then we could never query # these secondary data sources against, say, a calculated property because we're never turning # them into objects. FIXME - fix this by setting the $needs_further_boolexpr_evaluation_after_loading # flag maybe? my @secondary_data; foreach my $callback (@addl_join_comparators) { # FIXME - (no, not another one...) There's no mechanism for duplicating SQL join's # behavior where if a row from a table joins to 2 rows in the secondary table, the # first table's data will be in the result set twice. my $secondary_db_row = $callback->($next_db_row); unless (defined $secondary_db_row) { # That data source has no more data, so there can be no more joins even if the # primary data source has more data left to read $db_iterator = undef; $primary_object_for_next_db_row = undef; last LOAD_AN_OBJECT; } unless ($secondary_db_row) { # It returned 0 # didn't join (but there is still more data we can read later)... throw this row out. $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } # $next_db_row is a read-only value from DBI, so we need to track our additional # data seperately and smash them together before the object importer is called push(@secondary_data, @$secondary_db_row); } # get one or more objects from this row of results my $re_iterate = 0; my @imported; for (my $i = 0; $i < @object_fabricator_closures; $i++) { my $object_fabricator = $object_fabricator_closures[$i]; # The usual case is that the query is just against one data source, and so the importer # callback is just given the row returned from the DB query. For multiple data sources, # we need to smash together the primary and all the secondary lists my $imported_object; #my $object_creation_time; #if ($is_monitor_query) { # $object_creation_time = Time::HiRes::time(); #} if (@secondary_data) { $imported_object = $object_fabricator->([@$next_db_row, @secondary_data]); } else { $imported_object = $object_fabricator->($next_db_row); } #if ($is_monitor_query) { # $self->_log_query_for_rule($class_name, $rule, sprintf("QUERY: object fabricator took %.4f s",Time::HiRes::time() - $object_creation_time)); #} if ($imported_object and not ref($imported_object)) { # object requires sub-classsification in a way which involves different db data. $re_iterate = 1; } push @imported, $imported_object; # If the object ID for fabricator slot $i changes, then we can apply the # all_params_loaded changes from iterators 0 .. $i-1 because we know we've # loaded all the hangoff data related to the previous object # remember that the last fabricator in the list is for the primary object if (defined $imported_object and ref($imported_object)) { if (!defined $object_ids_from_fabricators[$i]) { $object_ids_from_fabricators[$i] = $imported_object->id; } elsif ($object_ids_from_fabricators[$i] ne $imported_object->id) { for (my $j = 0; $j < $i; $j++) { $object_fabricators[$j]->apply_all_params_loaded; } $object_ids_from_fabricators[$i] = $imported_object->id; } } } $primary_object_for_next_db_row = $imported[-1]; # The object importer will return undef for an object if no object # got created for that $next_db_row, and will return a string if the object # needs to be subclassed before being returned. Don't put serial numbers on # these map { $_->{'__get_serial'} = $this_get_serial } grep { defined && ref } @imported; if ($re_iterate and defined($primary_object_for_next_db_row) and ! ref($primary_object_for_next_db_row)) { # It is possible that one or more objects go into subclasses which require more # data than is on the results row. For each subclass (or set of subclasses), # we make a more specific, subordinate iterator to delegate-to. my $subclass_name = $primary_object_for_next_db_row; my $subclass_meta = UR::Object::Type->get(class_name => $subclass_name); my $table_subclass = $subclass_meta->most_specific_subclass_with_table(); my $sub_iterator = $subordinate_iterator_for_class{$table_subclass}; unless ($sub_iterator) { #print "parallel iteration for loading $subclass_name under $class_name!\n"; my $sub_classified_rule_template = $rule_template->sub_classify($subclass_name); my $sub_classified_rule = $sub_classified_rule_template->get_normalized_rule_for_values(@values); $sub_iterator = $subordinate_iterator_for_class{$table_subclass} = $self->_create_import_iterator_for_underlying_context($sub_classified_rule,$dsx,$this_get_serial); } ($primary_object_for_next_db_row) = $sub_iterator->(); if (! defined $primary_object_for_next_db_row) { # the newly subclassed object redo LOAD_AN_OBJECT; } } # end of handling a possible subordinate iterator delegate unless (defined $primary_object_for_next_db_row) { #if (!$primary_object_for_next_db_row or $rule->evaluate($primary_object_for_next_db_row)) { redo LOAD_AN_OBJECT; } if ( !$group_by and (ref($primary_object_for_next_db_row) ne $class_name) and (not $primary_object_for_next_db_row->isa($class_name)) ) { $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } if ($by_hand_recursive_source_property) { my @values = grep { defined } $primary_object_for_next_db_row->$by_hand_recursive_source_property; push @by_hand_recursive_source_values, @values; } if (! defined($next_object_to_return) or (Scalar::Util::refaddr($next_object_to_return) == Scalar::Util::refaddr($primary_object_for_next_db_row)) ) { # The first time through the iterator, we need to buffer the object until # $primary_object_for_next_db_row is something different. $next_object_to_return = $primary_object_for_next_db_row; $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } } # end of loop until we have a defined object to return #foreach my $object_fabricator ( @object_fabricators ) { # # Don't apply all_params_loaded for primary fab until it's all done # next if ($object_fabricator eq $object_fabricators[-1]); # $object_fabricator->apply_all_params_loaded; #} my $retval = $next_object_to_return; $next_object_to_return = $primary_object_for_next_db_row; return $retval; }; Sub::Name::subname('UR::Context::__underlying_context_iterator(closure)__', $underlying_context_iterator); return $underlying_context_iterator; } 1; LoadingIterator.pm100664023532023421 7201512544604516 20027 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::LoadingIterator; use strict; use warnings; use UR::Context; our $VERSION = "0.44"; # UR $VERSION; # A helper package for UR::Context to handling queries which require loading # data from outside the current context. It is responsible for collating # cached objects and incoming objects. When create_iterator() is used in # application code, this is the iterator that gets returned # # These are normal Perl objects, not UR objects, so they get regular # refcounting and scoping our @CARP_NOT = qw( UR::Context ); # A boolean flag used in the loading iterator to control whether we need to # inject loaded objects into other loading iterators' cached lists my $is_multiple_loading_iterators = 0; my %all_loading_iterators; # The set of objects returned by an iterator is initially determined when the # iterator is created, but the final determination of membership happens when # the object is about to be returned from the iterator's next() method. # In practice, this means that an object matches the BoolExpr at iterator # creation, and no longer matches when that object is about to be returned, # it will not be returned. # # If an object does not match the bx when the iterator is created, it will # not be returned even if it later changes to match before the iterator is # exhausted. # # If an object changes so that it's sort order changes after the iterator is # created but before it is returned by the iterator, the object will be # returned in the order it had at iterator creation time. # Finally, the LoadingIterator will throw an exception if an object matches # the BoolExpr at iterator creation time, but is deleted when next() is about # to return it (ie. isa UR::DeletedRef). Since DeletedRef's die any time you # try to use them, the object sorters can't sort them. Instead, we'll just # punt and throw an exception ourselves if we come across one. # # This seems like the least suprising thing to do, but there are other solutions: # 1) just plain don't return the deleted object # 2) use signal_change to register a callback which will remove objects being deleted # from all the in-process iterator @$cached lists (accomplishes the same as #1). # For completeness, this may imply that other signal_change callbacks would remove # objects that no longer match rules for in-process iterators, and that means that # next() returns things true at the time next() is called, not when the iterator # is created. # 3) Put in some additional infrastructure so we can pull out the ID of a deleted # object. That lets us call $next_object->id at the end of the closure, and return these # deleted objects back to the user. Problem being that the user then can't really # do anything with them. But it would be consistent about returning _all_ objects # that matched the rule at iterator creation time # 4) Like #3, but just always return the deleted object before any underlying_context # object, and then don't try to get its ID at the end if the iterator if it's deleted sub _create { my($class, $cached, $context, $normalized_rule, $data_source, $this_get_serial ) = @_; my $underlying_context_iterator = $context->_create_import_iterator_for_underlying_context( $normalized_rule, $data_source, $this_get_serial); my $is_monitor_query = $context->monitor_query; # These are captured by the closure... my($last_loaded_id, $next_obj_current_context, $next_obj_underlying_context); my $object_sorter = $normalized_rule->template->sorter(); my $bx_subject_class = $normalized_rule->subject_class_name; # Collection of object IDs that were read from the DB query. These objects are for-sure # not deleted, even though a cached object for it might have been turned into a ghost or # had its properties changed my %db_seen_ids_that_are_not_deleted; # Collection of object IDs that were read from the cached object list and haven't been # seen in the lsit of results from the database (yet). It could be missing from the DB # results because that row has been deleted, because the DB row still exists but has been # changed since we loaded it and now doesn't match the BoolExp, or because we're sorting # results by something other than just ID, that sorted property has been changed in the DB # and we haven't come across this row yet but will before. # # The short story is that if there is anything in this hash when the underlying context iterator # is exhausted, then the ID-ed object is really deleted, and should be an exception my %changed_objects_that_might_be_db_deleted; my $underlying_context_objects_count = 0; my $cached_objects_count = 0; # knowing if an object's changed properties are one of the rule's order-by # properties helps later on in the loading process of detecting deleted DB rows my %order_by_properties; if ($normalized_rule->template->order_by) { %order_by_properties = map { $_ => 1 } @{ $normalized_rule->template->order_by }; } my $change_is_order_by_property = sub { foreach my $prop_name ( shift->_changed_property_names ) { return 1 if exists($order_by_properties{$prop_name}); } return; }; my %bx_filter_properties = map { $_ => 1 } $normalized_rule->template->_property_names; my $change_is_bx_filter_property = sub { foreach my $prop_name ( shift->_changed_property_names ) { return 1 if exists($bx_filter_properties{$prop_name}); } return; }; my $limit = $normalized_rule->template->limit; my $offset = $normalized_rule->template->offset; my $me_loading_iterator_as_string; # See note below the closure definition my $loading_iterator = sub { return if (defined($limit) and !$limit); my $next_object; PICK_NEXT_OBJECT_FOR_LOADING: while (! defined($next_object)) { if ($underlying_context_iterator && ! defined($next_obj_underlying_context)) { ($next_obj_underlying_context) = $underlying_context_iterator->(1); $underlying_context_objects_count++ if ($is_monitor_query and defined($next_obj_underlying_context)); if (defined($next_obj_underlying_context)) { if ($next_obj_underlying_context->isa('UR::DeletedRef')) { # This object is deleted in the current context and not yet committed # skip it and pick again $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($next_obj_underlying_context->__changes__ and $change_is_order_by_property->($next_obj_underlying_context) ) { unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context->id}) { $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context->id} = 1; } $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } } unless (defined $next_obj_current_context) { ($next_obj_current_context) = shift @$cached; $cached_objects_count++ if ($is_monitor_query and $next_obj_current_context); } if (defined($next_obj_current_context) and $next_obj_current_context->isa('UR::DeletedRef')) { my $obj_to_complain_about = $next_obj_current_context; # undef it in case the user traps the exception, next time we'll pull another off the list $next_obj_current_context = undef; Carp::croak("Attempt to fetch an object which matched $normalized_rule when the iterator was created, " . "but was deleted in the meantime:\n" . Data::Dumper::Dumper($obj_to_complain_about) ); } if (!defined($next_obj_underlying_context)) { if ($is_monitor_query) { $context->_log_query_for_rule($bx_subject_class, $normalized_rule, "QUERY: loaded $underlying_context_objects_count object(s) total from underlying context."); } $underlying_context_iterator = undef; # Anything left in this hash when the DB iterator is exhausted are object we expected to # see by now and must be deleted. If any of these object have changes then # the __merge below will throw an exception foreach my $problem_obj (values(%changed_objects_that_might_be_db_deleted)) { $context->__merge_db_data_with_existing_object($bx_subject_class, $problem_obj, undef, []); } } elsif (defined($last_loaded_id) and $last_loaded_id eq $next_obj_underlying_context->id) { # during a get() with -hints or is_many+is_optional (ie. something with an # outer join), it's possible that the join can produce the same main object # as it's chewing through the (possibly) multiple objects joined to it. # Since the objects will be returned sorted by their IDs, we only have to # remember the last one we saw # FIXME - is this still true now that the underlying context iterator and/or # object fabricator hold off on returning any objects until all the related # joined data bas been loaded? $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } # decide which pending object to return next # both the cached list and the list from the database are sorted separately but with # equivalent algorithms (we hope). # # we're collating these into one return stream here my $comparison_result = undef; if (defined($next_obj_underlying_context) && defined($next_obj_current_context)) { $comparison_result = $object_sorter->($next_obj_underlying_context, $next_obj_current_context); } my $next_obj_underlying_context_id; $next_obj_underlying_context_id = $next_obj_underlying_context->id if (defined $next_obj_underlying_context); my $next_obj_current_context_id; $next_obj_current_context_id = $next_obj_current_context->id if (defined $next_obj_current_context); # This if() section is for when the in-memory and DB iterators return the same # object at the same time. if ( defined($next_obj_underlying_context) and defined($next_obj_current_context) and $comparison_result == 0 # $next_obj_underlying_context->id eq $next_obj_current_context->id ) { # Both objects sort the same. Since the ID properties are always last in the sort order list, # this means both objects must be the same object. $context->_log_query_for_rule($bx_subject_class, $normalized_rule, "QUERY: loaded object was already cached") if ($is_monitor_query); $next_object = $next_obj_current_context; $next_obj_current_context = undef; $next_obj_underlying_context = undef; } # This if() section is for when the DB iterator's object sorts first elsif ( defined($next_obj_underlying_context) and ( (!defined($next_obj_current_context)) or ($comparison_result < 0) # ($next_obj_underlying_context->id le $next_obj_current_context->id) ) ) { # db object sorts first # If we deleted it from memorym the DB would not have given it back. # So it either failed to match the BX now, or one of the order-by parameters changed if ($next_obj_underlying_context->__changes__) { # See if one of the changes is an order-by property if ($change_is_order_by_property->($next_obj_underlying_context)) { # If the object has changes, and one of the changes is one of the # order-by properties, then the object will: # 1) Already have appeared as $next_obj_current_context. # it will be in $changed_objects_that_might_be_db_deleted - remove it from that list # 2) Will appear later as $next_obj_current_context. # Mark here that it's not deleted unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context_id}) { $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context_id} = 1; } } elsif ($change_is_bx_filter_property->($next_obj_underlying_context)) { # If the object has any changes, then it will appear in the cached object list in # $next_object_current_context at the appropriate time. For the case where the # object no longer matches the BoolExpr, then the appropriate time is never. # Discard this object from the DB and pick again $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # some other kind of change? $next_object = $next_obj_underlying_context; $next_obj_underlying_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } else { # If the object has no changes, it must be something newly brought into the system. $next_object = $next_obj_underlying_context; $next_obj_underlying_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } # This if() section is for when the in-memory iterator's object sorts first elsif ( defined($next_obj_current_context) and ( (!defined($next_obj_underlying_context)) or ($comparison_result > 0) # ($next_obj_underlying_context->id ge $next_obj_current_context->id) ) ) { # The cached object sorts first # Either it was changed in memory, in the DB or both # In addition, the change could have been to an order-by property, one of the # properties in the BoolExpr, or both if (! $next_obj_current_context->isa('UR::Object::Set') # Sets aren't really from the underlying context and $context->object_exists_in_underlying_context($next_obj_current_context) ) { if ($next_obj_current_context->__changes__) { if ($change_is_order_by_property->($next_obj_current_context)) { # This object is expected to exist in the underlying context, has changes, and at # least one of those changes is to an order-by property # # if it's in %db_seen_ids_that_are_not_deleted, then it was seen earlier # from the DB, and can now be removed from that hash. unless (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) { # If not in that list, then add it to the list of things we might see later # in the DB iterator. If we don't see it by the end if the iterator, it # must have been deleted from the DB. At that time, we'll throw an exception. # It's later than we'd like, since the caller has already gotten ahold of the # object, but better late than never. The alternative is to do an id-only # query right now, but that would be inefficient. # # We could avoid storing this if we could verify that the db_committed/db_saved_uncommitted # values did NOT match the BoolExpr, but this will suffice for now. $changed_objects_that_might_be_db_deleted{$next_obj_current_context_id} = $next_obj_current_context; } # In any case, return the cached object. $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($change_is_bx_filter_property->($next_obj_current_context)) { # The change was that the object originally did not the filter, but since being # loaded it's been changed so it now matches the filter. The DB iterator isn't # returning the object since the DB's copy doesn't match the filter. delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}; $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } else { # The change is not an order-by property. This object must have been deleted # from the DB. The call to __merge below will throw an exception $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []); $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } else { # This cached object has no changes, so the database must have changed. # It could be deleted, no longer match the BoolExpr, or have changes in an order-by property if (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) { # We saw this already on the DB iterator. It's not deleted. Go ahead and return it $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($normalized_rule->is_id_only) { # If the query is id-only, and we didn't see the DB object at the same time, then # the DB row must have been deleted. Changing the PK columns in the DB are logically # the same as deleting the old object and creating/defineing a new one in UR. # # The __merge will delete the cached object, then pick again $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []); $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # Force an ID-only query to the underying context my $requery_obj = $context->reload($bx_subject_class, id => $next_obj_current_context_id); if ($requery_obj) { # In any case, the DB iterator will pull it up at the appropriate time, # and since the object has no changes, it will be returned to the caller then. # Discard this in-memory object and pick again $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # We've now confirmed that the object in the DB is really gone # NOTE: the reload() has already performed the __merge (implying deletion) # in the above branch "elsif ($normalized_rule->is_id_only)" so we don't need # to __merge/delete it here $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } } } else { # The object does not exist in the underlying context. It must be # a newly created object. $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } elsif (!defined($next_obj_current_context) and !defined($next_obj_underlying_context) ) { # Both iterators are exhausted. Bail out $next_object = undef; $last_loaded_id = undef; last PICK_NEXT_OBJECT_FOR_LOADING; } else { # Couldn't decide which to pick next? Something has gone horribly wrong. # We're using other vars to hold the objects and setting # $next_obj_current_context/$next_obj_underlying_context to undef so if # the caller is trapping exceptions, this iterator will pick new objects next time my $current_problem_obj = $next_obj_current_context; my $underlying_problem_obj = $next_obj_underlying_context; $next_obj_current_context = undef; $next_obj_underlying_context = undef; $next_object = undef; Carp::croak("Loading iterator internal error. Could not pick a next object for loading.\n" . "Next object underlying context: " . Data::Dumper::Dumper($underlying_problem_obj) . "\nNext object current context: ". Data::Dumper::Dumper($current_problem_obj)); } return unless defined $next_object; # end while ! $next_object } continue { if (defined($next_object) and defined($offset) and $offset) { $offset--; $next_object = undef; } } $last_loaded_id = $next_object->id if (defined $next_object); $limit-- if defined $limit; return $next_object; }; # end of the closure bless $loading_iterator, $class; Sub::Name::subname($class . '__loading_iterator_closure__', $loading_iterator); # Inside the closure, it needs to know its own address, but without holding a real reference # to itself - otherwise the closure would never go out of scope, the destructor would never # get called, and the list of outstanding loaders would never get pruned. This way, the closure # holds a reference to the string version of its address, which is the only thing it really # needed anyway $me_loading_iterator_as_string = $loading_iterator . ''; $all_loading_iterators{$me_loading_iterator_as_string} = [ $me_loading_iterator_as_string, $normalized_rule, $object_sorter, $cached, \$underlying_context_objects_count, \$cached_objects_count, $context, ]; $is_multiple_loading_iterators = 1 if (keys(%all_loading_iterators) > 1); return $loading_iterator; } # end _create() sub DESTROY { my $self = shift; my $iter_data = $all_loading_iterators{$self}; if ($iter_data->[0] eq $self) { # that's me! # Items in the listref are: $loading_iterator_string, $rule, $object_sorter, $cached, # \$underlying_context_objects_count, \$cached_objects_count, $context my $context = $iter_data->[6]; if ($context and $context->monitor_query) { my $rule = $iter_data->[1]; my $count = ${$iter_data->[4]} + ${$iter_data->[5]}; $context->_log_query_for_rule($rule->subject_class_name, $rule, "QUERY: Query complete after returning $count object(s) for rule $rule."); $context->_log_done_elapsed_time_for_rule($rule); } delete $all_loading_iterators{$self}; $is_multiple_loading_iterators = 0 if (keys(%all_loading_iterators) < 2); } else { Carp::carp('A loading iterator went out of scope, but could not be found in the registered list of iterators'); } } # Used by the loading itertor to inject a newly loaded object into another # loading iterator's @$cached list. This is to handle the case where the user creates # an iterator which will load objects from the DB. Before all the data from that # iterator is read, another get() or iterator is created that covers (some of) the same # objects which get pulled into the object cache, and the second request is run to # completion. Since the underlying context iterator has been changed to never return # objects currently cached, the first iterator would have incorrectly skipped ome objects that # were not loaded when the first iterator was created, but later got loaded by the second. sub _inject_object_into_other_loading_iterators { my($self, $new_object, $iterator_to_skip) = @_; ITERATOR: foreach my $iter_name ( keys %all_loading_iterators ) { next if $iter_name eq $iterator_to_skip; # That's me! Don't insert into our own @$cached this way my($loading_iterator, $rule, $object_sorter, $cached) = @{$all_loading_iterators{$iter_name}}; if ($rule->evaluate($new_object)) { my $cached_list_len = @$cached; for(my $i = 0; $i < $cached_list_len; $i++) { my $cached_object = $cached->[$i]; next if $cached_object->isa('UR::DeletedRef'); my $comparison = $object_sorter->($new_object, $cached_object); if ($comparison < 0) { # The new object sorts sooner than this one. Insert it into the list splice(@$cached, $i, 0, $new_object); next ITERATOR; } elsif ($comparison == 0) { # This object is already in the list next ITERATOR; } } # It must go at the end... push @$cached, $new_object; } } # end foreach } # Reverse of _inject_object_into_other_loading_iterators(). Used when one iterator detects that # a previously loaded object no longer exists in the underlying context/datasource sub _remove_object_from_other_loading_iterators { my($self, $disappearing_object, $iterator_to_skip) = @_; ITERATOR: foreach my $iter_name ( keys %all_loading_iterators ) { next if(! defined $iterator_to_skip or ($iter_name eq $iterator_to_skip)); # That's me! Don't remove into our own @$cached this way my($loading_iterator, $rule, $object_sorter, $cached) = @{$all_loading_iterators{$iter_name}}; next if (defined($iterator_to_skip) and $loading_iterator eq $iterator_to_skip); # That's me! Don't insert into our own @$cached this way if ($rule->evaluate($disappearing_object)) { my $cached_list_len = @$cached; for(my $i = 0; $i < $cached_list_len; $i++) { my $cached_object = $cached->[$i]; next if $cached_object->isa('UR::DeletedRef'); my $comparison = $object_sorter->($disappearing_object, $cached_object); if ($comparison == 0) { # That's the one, remove it from the list splice(@$cached, $i, 1); next ITERATOR; } elsif ($comparison < 0) { # past the point where we expect to find this object next ITERATOR; } } } } # end foreach } # Returns true if any of the object's changed properites are keys # in the passed-in hashref. Used by the Loading Iterator to find out if # a change is one of the order-by properties of a bx sub _changed_property_in_hash { my($self,$object,$hash) = @_; foreach my $prop_name ( $object->_changed_property_names ) { return 1 if (exists $hash->{$prop_name}); } return; } 1; ObjectFabricator.pm100664023532023421 15475712544604516 20201 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::ObjectFabricator; use strict; use warnings; use Scalar::Util; use UR::Context; our $VERSION = "0.44"; # UR $VERSION; # A helper package for UR::Context to keep track of the subrefs used # to create objects from database data # These are normal Perl objects, not UR objects, so they get # regular refcounting and scoping our @CARP_NOT = qw( UR::Context ); my %all_object_fabricators; sub _create { my $class = shift; my %params = @_; unless ($params{'fabricator'} and ref($params{'fabricator'}) eq 'CODE') { Carp::croak("UR::Context::ObjectFabricator::create requires a subroutine ref for the 'fabricator' parameter"); } unless ($params{'context'} and ref($params{'context'}) and $params{'context'}->isa('UR::Context')) { Carp::croak("UR::Context::ObjectFabricator::create requires a UR::Context object for the 'context' parameter"); } my $self = bless {}, $class; $self->{'fabricator'} = $params{'fabricator'}; $self->{'context'} = $params{'context'}; $self->{'all_params_loaded'} = $params{'all_params_loaded'} || {}; $self->{'in_clause_values'} = $params{'in_clause_values'} || {}; $all_object_fabricators{$self} = $self; Scalar::Util::weaken($all_object_fabricators{$self}); return $self; } sub create_for_loading_template { my($fab_class, $context, $loading_template, $query_plan, $rule, $rule_template, $values, $dsx) = @_; my @values = @$values; my $class_name = $loading_template->{final_class_name}; #$class_name or Carp::croak("No final_class_name in loading template?"); unless ($class_name) { #Carp::carp("No final_class_name in loading template for rule $rule"); return; # This join doesn't result in an object? - i think this happens when you do a get() with -hints } my $class_meta = $class_name->__meta__; my $class_data = $dsx->_get_class_data_for_loading($class_meta); my $class = $class_name; my $ghost_class = $class_data->{ghost_class}; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my $sub_classification_method_name = $class_data->{sub_classification_method_name}; # FIXME, right now, we don't have a rule template for joined entities... my $rule_template_id = $query_plan->{rule_template_id}; my $rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc}; my $rule_template_id_without_recursion_desc = $query_plan->{rule_template_id_without_recursion_desc}; my $rule_matches_all = $query_plan->{rule_matches_all}; my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only}; my $rule_specifies_id = $query_plan->{rule_specifies_id}; my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype}; my $recursion_desc = $query_plan->{recursion_desc}; my $recurse_property_on_this_row = $query_plan->{recurse_property_on_this_row}; my $recurse_property_referencing_other_rows = $query_plan->{recurse_property_referencing_other_rows}; my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'}; my $rule_id = $rule->id; my $rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values); my $loading_base_object; if ($loading_template == $query_plan->{loading_templates}[0]) { $loading_base_object = 1; } else { $loading_base_object = 0; $needs_further_boolexpr_evaluation_after_loading = 0; } my %subclass_is_safe_for_re_bless; my %subclass_for_subtype_name; my %recurse_property_value_found; my @property_names = @{ $loading_template->{property_names} }; my @id_property_names = @{ $loading_template->{id_property_names} }; my @column_positions = @{ $loading_template->{column_positions} }; my @id_positions = @{ $loading_template->{id_column_positions} }; my $multi_column_id = (@id_positions > 1 ? 1 : 0); my $composite_id_resolver = $class_meta->get_composite_id_resolver; # The old way of specifying that some values were constant for all objects returned # by a get(). The data source would wrap the method that builds the loading template # and wedge in some constant_property_names. The new way is to add columns to the # loading template, and then add the values onto the list returned by the data source # iterator. my %initial_object_data; if ($loading_template->{constant_property_names}) { my @constant_property_names = @{ $loading_template->{constant_property_names} }; my @constant_property_values = map { $rule->value_for($_) } @constant_property_names; @initial_object_data{@constant_property_names} = @constant_property_values; } my $rule_class_name = $rule_template->subject_class_name; my $template_id = $rule_template->id; my $load_class_name = $class; # $rule can contain params that may not apply to the subclass that's currently loading. # define_boolexpr() in array context will return the portion of the rule that actually applies #my($load_rule, undef) = $load_class_name->define_boolexpr($rule->params_list); my($load_rule, @extra_params) = UR::BoolExpr->resolve($load_class_name, $rule->params_list); my $load_rule_id = $load_rule->id; my $load_template_id = $load_rule->template_id; my @rule_properties_with_in_clauses = grep { $rule_template_without_recursion_desc->operator_for($_) eq 'in' } $rule_template_without_recursion_desc->_property_names; my($rule_template_without_in_clause,$rule_template_id_without_in_clause,%in_clause_values,@all_rule_property_names); my $do_record_in_all_params_loaded = 1; if (@rule_properties_with_in_clauses) { $rule_template_id_without_in_clause = $rule_template_without_recursion_desc->id; foreach my $property_name ( @rule_properties_with_in_clauses ) { # FIXME - removing and re-adding the filter should have the same effect as the substitute below, # but the two result in different rules in the end. #$rule_template_without_in_clause = $rule_template_without_in_clause->remove_filter($property_name); #$rule_template_without_in_clause = $rule_template_without_in_clause->add_filter($property_name); $rule_template_id_without_in_clause =~ s/($property_name) in/$1/; } $rule_template_without_in_clause = UR::BoolExpr::Template->get($rule_template_id_without_in_clause); # Make a note of all the values in the in-clauses. As the objects get returned from the # data source, we'll remove these notes. Anything that's left by the time the iterator is # finalized must be values that matched nothing. Then, finalize can put data in # all_params_loaded showing it matches nothing my %rule_properties_with_in_clauses = map { $_ => 1 } @rule_properties_with_in_clauses; @all_rule_property_names = $rule_template_without_in_clause->_property_names; foreach my $property ( @rule_properties_with_in_clauses ) { my $values_for_in_clause = $rule_without_recursion_desc->value_for($property); unless ($values_for_in_clause) { Carp::confess("rule has no value for property $property: $rule_without_recursion_desc"); } if (@$values_for_in_clause > 100) { $do_record_in_all_params_loaded = 0; next; } my @other_values = map { exists $rule_properties_with_in_clauses{$_} ? undef # placeholder filled in below : $rule_without_recursion_desc->value_for($_) } $rule_template_without_in_clause->_property_names; my $position_for_this_property = $rule_template_without_in_clause->value_position_for_property_name($property); # If the number of items in the in-clause is over this number, then don't bother recording # the template-id/rule-id, since searching the list to see if this query has been done before # is going to take longer than just re-doing the query foreach my $value ( @$values_for_in_clause ) { $value = '' if (!defined $value); $other_values[$position_for_this_property] = $value; my $rule_with_this_in_property = $rule_template_without_in_clause->get_rule_for_values(@other_values); $in_clause_values{$property}->{$value} = [$rule_template_id_without_in_clause, $rule_with_this_in_property->id]; } } } # This is a local copy of what we want to put in all_params_loaded, when the object fabricator is # finalized my $local_all_params_loaded = {}; my($hints_or_delegation,$delegations_with_no_objects); if (!$loading_base_object) { ($hints_or_delegation,$delegations_with_no_objects) = $fab_class->_resolve_delegation_data($rule,$loading_template,$query_plan,$local_all_params_loaded); } my $update_apl_for_loaded_object = sub { my $pending_db_object = shift; # Make a note in all_params_loaded (essentially, the query cache) that we've made a # match on this rule, and some equivalent rules if ($loading_base_object and not $rule_specifies_id) { if ($do_record_in_all_params_loaded) { if ($rule_class_name ne $load_class_name and scalar(@extra_params) == 0) { $pending_db_object->{__load}->{$load_template_id}{$load_rule_id}++; $UR::Context::all_params_loaded->{$load_template_id}{$load_rule_id} = undef; $local_all_params_loaded->{$load_template_id}{$load_rule_id}++; } $pending_db_object->{__load}->{$template_id}{$rule_id}++; $UR::Context::all_params_loaded->{$template_id}{$rule_id} = undef; $local_all_params_loaded->{$template_id}{$rule_id}++; } if (@rule_properties_with_in_clauses) { # FIXME - confirm that all the object properties are filled in at this point, right? #my @values = @$pending_db_object{@rule_properties_with_in_clauses}; my @values = @$pending_db_object{@all_rule_property_names}; my $r = $rule_template_without_in_clause->get_normalized_rule_for_values(@values); my $r_id = $r->id; $UR::Context::all_params_loaded->{$rule_template_id_without_in_clause}{$r_id} = undef; $local_all_params_loaded->{$rule_template_id_without_in_clause}{$r_id}++; # remove the notes about these in-clause values since they matched something no warnings; # undef treated as an empty string below foreach my $property (@rule_properties_with_in_clauses) { my $value = $pending_db_object->{$property}; delete $in_clause_values{$property}->{$value}; } } } }; my $fabricator_obj; # filled in after the closure definition my $object_fabricator = sub { my $next_db_row = $_[0]; # If all the columns for this object are undef, then this doesn't encode an actual # object, it's a result of a left join that matched nothing my $values_exist; foreach my $column ( @column_positions ) { if (defined($next_db_row->[$column])) { $values_exist = 1; last; } } if (!$loading_base_object and !$values_exist and $delegations_with_no_objects) { my $templates_and_rules = $fab_class->_lapl_data_for_delegation_data($delegations_with_no_objects, $next_db_row); while ( my($template_id, $rule_id) = each %$templates_and_rules) { $local_all_params_loaded->{$template_id}->{$rule_id} = 0; $UR::Context::all_params_loaded->{$template_id}->{$rule_id} = 0; } return; } my $pending_db_object_data = { %initial_object_data }; @$pending_db_object_data{@property_names} = @$next_db_row[@column_positions]; # resolve id my $pending_db_object_id; if ($multi_column_id) { $pending_db_object_id = $composite_id_resolver->(@$pending_db_object_data{@id_property_names}) } else { $pending_db_object_id = $pending_db_object_data->{$id_property_names[0]}; } unless (defined $pending_db_object_id) { return undef; Carp::confess( "no id found in object data for $class_name?\n" . Data::Dumper::Dumper($pending_db_object_data) ); } my $pending_db_object; # skip if this object has been deleted but not committed do { no warnings; if ($UR::Context::all_objects_loaded->{$ghost_class}{$pending_db_object_id}) { return; #$pending_db_object = undef; #redo; } }; # Handle the object based-on whether it is already loaded in the current context. if ($pending_db_object = $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id}) { $context->__merge_db_data_with_existing_object($class, $pending_db_object, $pending_db_object_data, \@property_names); if ($loading_base_object and $needs_further_boolexpr_evaluation_after_loading and not $rule->evaluate($pending_db_object) ) { return; } $update_apl_for_loaded_object->($pending_db_object); } else { # Handle the case in which the object is completely new in the current context. # Create a new object for the resultset row $pending_db_object = bless { %$pending_db_object_data, id => $pending_db_object_id }, $class; $pending_db_object->{db_committed} = $pending_db_object_data; # determine the subclass name for classes which automatically sub-classify my $subclass_name; if ( ( $sub_classification_method_name or $subclassify_by or $sub_classification_meta_class_name ) and (ref($pending_db_object) eq $class) # not already subclased ) { if ($sub_classification_method_name) { $subclass_name = $class->$sub_classification_method_name($pending_db_object); unless ($subclass_name) { my $pending_obj_id = eval { $pending_db_object->id }; Carp::confess( "Object with id '$pending_obj_id' loaded as abstract class $class failed to subclassify itself using method " . $sub_classification_method_name ); } } elsif ($sub_classification_meta_class_name) { # Group objects requiring reclassification by type, # and catch anything which doesn't need reclassification. my $subtype_name = $pending_db_object->$subclassify_by; $subclass_name = $subclass_for_subtype_name{$subtype_name}; unless ($subclass_name) { my $type_obj = $sub_classification_meta_class_name->get($subtype_name); unless ($type_obj) { # The base type may give the final subclass, or an intermediate # either choice has trade-offs, but we support both. # If an intermediate subclass is specified, that subclass # will join to a table with another field to indicate additional # subclassing. This means we have to do this part the hard way. # TODO: handle more than one level. my @all_type_objects = $sub_classification_meta_class_name->get(); for my $some_type_obj (@all_type_objects) { my $some_subclass_name = $some_type_obj->subclass_name($class); unless (UR::Object::Type->get($some_subclass_name)->is_abstract) { next; } my $some_subclass_meta = $some_subclass_name->__meta__; my $some_subclass_type_class = $some_subclass_meta->sub_classification_meta_class_name; if ($type_obj = $some_subclass_type_class->get($subtype_name)) { # this second-tier subclass works last; } else { # try another subclass, and check the subclasses under it #print "skipping $some_subclass_name: no $subtype_name for $some_subclass_type_class\n"; } } } if ($type_obj) { $subclass_name = $type_obj->subclass_name($class); } else { warn "Failed to find $class_name sub-class for type '$subtype_name'!"; $subclass_name = $class_name; } unless ($subclass_name) { Carp::confess( "Failed to sub-classify $class using " . $type_obj->class . " '" . $type_obj->id . "'" ); } $subclass_name->class; } $subclass_for_subtype_name{$subtype_name} = $subclass_name; } else { $subclass_name = $pending_db_object->$subclassify_by; unless ($subclass_name) { Carp::croak("Failed to sub-classify $class while loading; calling method " . "'$subclassify_by' returned false. Relevant object data: " . Data::Dumper::Dumper($pending_db_object)); } } # note: we check this again with the real base class, but this keeps junk objects out of the core hash unless ($subclass_name->isa($class)) { # We may have done a load on the base class, and not been able to use properties to narrow down to the correct subtype. # The resultset returned more data than we needed, and we're filtering out the other subclasses here. return; } } else { # regular, non-subclassifier $subclass_name = $class; } # store the object # note that we do this on the base class even if we know it's going to be put into a subclass below $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id} = $pending_db_object; $UR::Context::all_objects_cache_size++; # If we're using a light cache, weaken the reference. if ($UR::Context::light_cache and substr($class,0,5) ne 'App::') { Scalar::Util::weaken($UR::Context::all_objects_loaded->{$class_name}->{$pending_db_object_id}); } $update_apl_for_loaded_object->($pending_db_object); my $boolexpr_evaluated_ok; if ($subclass_name eq $class) { # This object doesn't need additional subclassing # Signal that the object has been loaded # NOTE: until this is done indexes cannot be used to look-up an object $pending_db_object->__signal_change__('load'); } else { # we did this above, but only checked the base class my $subclass_ghost_class = $subclass_name->ghost_class; if ($UR::Context::all_objects_loaded->{$subclass_ghost_class}{$pending_db_object_id}) { # We put it in the object cache a few lines above. # FIXME - why not wait until we know we're keeping it before putting it in there? delete $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id}; $UR::Context::all_objects_cache_size--; return; #$pending_db_object = undef; #redo; } my $re_bless = $subclass_is_safe_for_re_bless{$subclass_name}; if (not defined $re_bless) { $re_bless = $dsx->_class_is_safe_to_rebless_from_parent_class($subclass_name, $class); $re_bless ||= 0; $subclass_is_safe_for_re_bless{$subclass_name} = $re_bless; } my $loading_info; if (!$re_bless) { # This object cannot just be re-classified into a subclass because the subclass joins to additional tables. # We'll make a parallel iterator for each subclass we encounter. # Note that we let the calling db-based iterator do that, so that if multiple objects on the row need # sub-classing, we do them all at once. # Decrement all of the param_keys it is using. if ($loading_base_object) { $loading_info = $dsx->_get_object_loading_info($pending_db_object); $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } #$pending_db_object->unload; delete $UR::Context::all_objects_loaded->{$class}->{$pending_db_object_id}; if ($loading_base_object) { $dsx->_record_that_loading_has_occurred($loading_info); } # NOTE: we're returning a class name instead of an object # this tells the caller to re-do the entire row using a subclass to get the real data. # Hack? Probably so... return $subclass_name; } # Performance shortcut. # These need to be subclassed, but there is no additional data to load. # Just remove from the object cache, rebless to the proper subclass, and # re-add to the object cache my $already_loaded = $subclass_name->is_loaded($pending_db_object_id); my $different; my $merge_exception; if ($already_loaded) { eval { $different = $context->__merge_db_data_with_existing_object($class, $already_loaded, $pending_db_object_data, \@property_names) }; $merge_exception = $@; } if ($already_loaded and !$different and !$merge_exception) { if ($pending_db_object == $already_loaded) { Carp::croak("An object of type ".$already_loaded->class." with ID '".$already_loaded->id ."' was just loaded, but already exists in the object cache in the proper subclass"); } if ($loading_base_object) { # Get our records about loading this object $loading_info = $dsx->_get_object_loading_info($pending_db_object); # Transfer the load info for the load we _just_ did to the subclass too. my $subclassified_template = $rule_template->sub_classify($subclass_name); $loading_info->{$subclassified_template->id} = $loading_info->{$template_id}; $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } # This will wipe the above data from the object and the contex... delete $UR::Context::all_objects_loaded->{$class}->{$pending_db_object_id}; if ($loading_base_object) { # ...now we put it back for both. $dsx->_add_object_loading_info($already_loaded, $loading_info); $dsx->_record_that_loading_has_occurred($loading_info); } bless($pending_db_object,'UR::DeletedRef'); $pending_db_object = $already_loaded; } else { if ($loading_base_object) { my $subclassified_template = $rule_template->sub_classify($subclass_name); $loading_info = $dsx->_get_object_loading_info($pending_db_object); $dsx->_record_that_loading_has_occurred($loading_info); $loading_info->{$subclassified_template->id} = delete $loading_info->{$template_id}; $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } my $prev_class_name = $pending_db_object->class; #my $id = $pending_db_object->id; #$pending_db_object->__signal_change__("unload"); delete $UR::Context::all_objects_loaded->{$prev_class_name}->{$pending_db_object_id}; delete $UR::Context::all_objects_are_loaded->{$prev_class_name}; if ($merge_exception) { # Now that we've removed traces of the incorrectly-subclassed $pending_db_object, # we can pass up any exception generated in __merge_db_data_with_existing_object Carp::croak($merge_exception); } if ($already_loaded) { # The new object should replace the old object. Since other parts of the user's program # may have references to this object, we need to copy the values from the new object into # the existing cached object bless($pending_db_object,'UR::DeletedRef'); $pending_db_object = $already_loaded; } else { # This is a completely new object $UR::Context::all_objects_loaded->{$subclass_name}->{$pending_db_object_id} = $pending_db_object; } bless $pending_db_object, $subclass_name; $pending_db_object->__signal_change__("load"); $dsx->_add_object_loading_info($pending_db_object, $loading_info); $dsx->_record_that_loading_has_occurred($loading_info); } # the object may no longer match the rule after subclassifying... if ($needs_further_boolexpr_evaluation_after_loading and $loading_base_object and not $rule->evaluate($pending_db_object) ) { #print "Object does not match rule!" . Dumper($pending_db_object,[$rule->params_list]) . "\n"; #$rule->evaluate($pending_db_object); return; } else { $boolexpr_evaluated_ok = 1; } } # end of sub-classification code if ( $loading_base_object and $needs_further_boolexpr_evaluation_after_loading and ( ! $boolexpr_evaluated_ok ) and ( ! $rule->evaluate($pending_db_object) ) ) { return; } } # end handling newly loaded objects # If the rule had hints, mark that we loaded those things too, in all_params_loaded if ($hints_or_delegation) { my $templates_and_rules = $fab_class->_lapl_data_for_delegation_data($hints_or_delegation, $next_db_row, $pending_db_object); while ( my($template_id, $rule_id) = each %$templates_and_rules) { $local_all_params_loaded->{$template_id}->{$rule_id}++; $UR::Context::all_params_loaded->{$template_id}->{$rule_id} = undef; } } # note all of the joins which follow this object as having been "done" if (my $next_joins = $loading_template->{next_joins}) { if (0) { # disabled until a fully reframed query is the basis for these joins for my $next_join (@$next_joins) { my ($bxt_id, $values, $value_position_property_name) = @$next_join; for (my $n = 0; $n < @$value_position_property_name; $n+=2) { my $pos = $value_position_property_name->[$n]; my $name = $value_position_property_name->[$n+1]; $values->[$pos] = $pending_db_object->$name; } my $bxt = UR::BoolExpr::Template::And->get($bxt_id); my $bx = $bxt->get_rule_for_values(@$values); $UR::Context::all_params_loaded->{$bxt->{id}}->{$bx->{id}} = undef; $local_all_params_loaded->{$bxt->{id}}->{$bx->{id}}++; print "remembering $bx\n"; } } } # When there is recursion in the query, we record data from each # recursive "level" as though the query was done individually. if ($recursion_desc and $loading_base_object) { # if we got a row from a query, the object must have # a db_committed or db_saved_committed my $dbc = $pending_db_object->{db_committed} || $pending_db_object->{db_saved_uncommitted}; Carp::croak("Loaded database data has no save data for $class id ".$pending_db_object->id .". Something bad happened.".Data::Dumper::Dumper($pending_db_object)) unless $dbc; my $value_by_which_this_object_is_loaded_via_recursion = $dbc->{$recurse_property_on_this_row}; my $value_referencing_other_object = $dbc->{$recurse_property_referencing_other_rows}; $value_referencing_other_object = '' unless (defined $value_referencing_other_object); unless ($recurse_property_value_found{$value_referencing_other_object}) { # This row points to another row which will be grabbed because the query is hierarchical. # Log the smaller query which would get the hierarchically linked data directly as though it happened directly. $recurse_property_value_found{$value_referencing_other_object} = 1; # note that the direct query need not be done again my $equiv_rule = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_referencing_other_object, ); my $equiv_rule_id = $equiv_rule->id; my $equiv_template_id = $equiv_rule->template_id; # note that the recursive query need not be done again my $equiv_rule_2 = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_referencing_other_object, -recurse => $recursion_desc, ); my $equiv_rule_id_2 = $equiv_rule_2->id; my $equiv_template_id_2 = $equiv_rule_2->template_id; # For any of the hierarchically related data which is already loaded, # note on those objects that they are part of that query. These may have loaded earlier in this # query, or in a previous query. Anything NOT already loaded will be hit later by the if-block below. my @subset_loaded = $class->is_loaded($recurse_property_on_this_row => $value_referencing_other_object); $UR::Context::all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = undef; $UR::Context::all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = undef; $local_all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = scalar(@subset_loaded); $local_all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = scalar(@subset_loaded); for my $pending_db_object (@subset_loaded) { $pending_db_object->{__load}->{$equiv_template_id}->{$equiv_rule_id}++; $pending_db_object->{__load}->{$equiv_template_id_2}->{$equiv_rule_id_2}++; } } # NOTE: if it were possible to use undef values in a connect-by, this could be a problem # however, connect by in UR is always COL = COL, which would always fail on NULLs. if (defined($value_by_which_this_object_is_loaded_via_recursion) and $recurse_property_value_found{$value_by_which_this_object_is_loaded_via_recursion}) { # This row was expected because some other row in the hierarchical query referenced it. # Up the object count, and note on the object that it is a result of this query. my $equiv_rule = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_by_which_this_object_is_loaded_via_recursion, ); my $equiv_rule_id = $equiv_rule->id; my $equiv_template_id = $equiv_rule->template_id; # note that the recursive query need not be done again my $equiv_rule_2 = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_by_which_this_object_is_loaded_via_recursion, -recurse => $recursion_desc ); my $equiv_rule_id_2 = $equiv_rule_2->id; my $equiv_template_id_2 = $equiv_rule_2->template_id; $UR::Context::all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = undef; $UR::Context::all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = undef; $local_all_params_loaded->{$equiv_template_id}->{$equiv_rule_id}++; $local_all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2}++; $pending_db_object->{__load}->{$equiv_template_id}->{$equiv_rule_id}++; $pending_db_object->{__load}->{$equiv_template_id_2}->{$equiv_rule_id_2}++; } } # end of handling recursion return $pending_db_object; }; # end of per-class object fabricator Sub::Name::subname("UR::Context::__object_fabricator(closure)__ ($class_name)", $object_fabricator); # remember all the changes to $UR::Context::all_params_loaded that should be made. # This fixes the problem where you create an iterator for a query, read back some of # the items, but not all, then later make the same query. The old behavior made # entries in all_params_loaded as objects got loaded from the DB, so that at the time # the second query is made, UR::Context::_cache_is_complete_for_class_and_normalized_rule() # sees there are entries in all_params_loaded, and so reports yes, the cache is complete, # and the second query only returns the objects that were loaded during the first query. # # The new behavior builds up changes to be made to all_params_loaded, and someone # needs to call $object_fabricator->finalize() to apply these changes $fabricator_obj = $fab_class->_create(fabricator => $object_fabricator, context => $context, all_params_loaded => $local_all_params_loaded, in_clause_values => \%in_clause_values); return $fabricator_obj; } # Given the data created in _resolve_delegation_data (rule templates and values/valuerefs) # return a hash of template IDs => rule IDs that need to be manipulated in local_all_params_loaded sub _lapl_data_for_delegation_data { my($fab_class, $delegation_data_list, $next_db_row, $pending_db_object) = @_; my %tmpl_and_rules; #$DB::single=1; foreach my $delegation_data ( @$delegation_data_list ) { my $value_sources = $delegation_data->[0]; my $rule_tmpl = $delegation_data->[1]; my @values; foreach my $value_source ( @$value_sources ) { if (! ref($value_source)) { push @values, $value_source; } elsif (Scalar::Util::looks_like_number($$value_source)) { push @values, $next_db_row->[$$value_source]; } elsif ($pending_db_object) { my $method_name = $$value_source; my $result = eval { $pending_db_object->$method_name }; push @values, $result; } else { Carp::croak("Can't resolve value for '".$$value_source."' in delegation data when there is no object involved"); } } my $rule = $rule_tmpl->get_normalized_rule_for_values(@values); $tmpl_and_rules{$rule_tmpl->id} = $rule->id; } return \%tmpl_and_rules; } # This is used by fabricators created as a result of filters or hints on delegated properties # of the primary object to pre-calculate rule templates and value sources that can be combined # with these templates to make rules. The resulting template and rule IDs are then plugged into # all_params_loaded to indicate these related objects are loaded so that subsequent queries # will not hit the data sources. sub _resolve_delegation_data { my($fab_class,$rule,$loading_template,$query_plan,$local_all_params_loaded) = @_; my $rule_template = $rule->template; my $query_class_meta = $rule_template->subject_class_name->__meta__; my %hints; if ($rule_template->hints) { $hints{$_} = 1 foreach(@{ $rule_template->hints }); } my %delegations; if (@{ $query_plan->{'joins'}} ) { foreach my $delegated_property_name ( $rule_template->_property_names ) { my $delegated_property_meta = $query_class_meta->property_meta_for_name($delegated_property_name); next unless ($delegated_property_meta and $delegated_property_meta->is_delegated); $delegations{$delegated_property_name} = 1; } } my $this_object_num = $loading_template->{'object_num'}; my $join = $query_plan->_get_alias_join($loading_template->{'table_alias'}); return unless $join; # would this ever be false? return unless ($join->{'foreign_class'} eq $loading_template->{'data_class_name'}); # sanity check my $delegated_property_meta; # Find out which delegation property was responsible for this object being loaded DELEGATIONS: foreach my $delegation ((keys %hints), (keys %delegations)) { my $query_property_meta = $query_class_meta->property_meta_for_name($delegation); next DELEGATIONS unless $query_property_meta; my @joins_from_delegation = $query_property_meta->_resolve_join_chain(); foreach my $join_from_delegation ( @joins_from_delegation ) { if ($join_from_delegation->id eq $join->id) { $delegated_property_meta = $query_property_meta; last DELEGATIONS; } } } return unless $delegated_property_meta; my $delegated_property_name = $delegated_property_meta->property_name; return if $join->destination_is_all_id_properties(); my @template_filter_names = @{$join->{'foreign_property_names'}}; my %template_filter_values; foreach my $name ( @template_filter_names ) { my $column_num = $query_plan->column_index_for_class_property_and_object_num( $join->{'foreign_class'}, $name, $this_object_num); if (defined $column_num) { $template_filter_values{$name} = \$column_num; } else { my $prop_name = $name; $template_filter_values{$name} = \$prop_name; } } if ($delegations{$delegated_property_name}) { my $delegation_final_property_meta = $delegated_property_meta->final_property_meta; if ($delegation_final_property_meta and $delegation_final_property_meta->class_name eq $join->{'foreign_class'} ) { # This delegation points to (or at least through) this join's foreign class # We'll note that these related objects were loaded as a result of being # connected to the primary object by this value, and filtered by the # delegation property's value my $delegation_final_property_name = $delegation_final_property_meta->property_name; my $column_num = $query_plan->column_index_for_class_property_and_object_num( $join->{'foreign_class'}, $delegation_final_property_name, $this_object_num); push @template_filter_names, $delegation_final_property_name; if ($delegation_final_property_meta->column_name and ! defined ($column_num)) { # sanity check Carp::carp("Could not determine column offset in result set for property " . "$delegation_final_property_name of class " . $join->{'foreign_class'} . " even though it has column_name " . $delegation_final_property_meta->column_name); $column_num = undef; } if (defined $column_num) { $template_filter_values{$delegation_final_property_name} = \$column_num; } else { $template_filter_values{$delegation_final_property_name} = \$delegation_final_property_name; } } } # For missing objects, ie. a left join was done and it matched nothing my @missing_prop_names; my %missing_values; for (my $i = 0; $i < @{ $join->{'foreign_property_names'}}; $i++) { # we're using the source class/property here because we're going to denote that a value # of the source class of the join matched nothing my $prop_name = $join->{'foreign_property_names'}->[$i]; push @missing_prop_names, $prop_name; my $source_class = $join->{'source_class'}; my $source_prop_name = $join->{'source_property_names'}->[$i]; my $column_num; if( my($actual_prop_meta) = $source_class->__meta__->_concrete_property_meta_for_class_and_name($source_prop_name) ) { $column_num = $query_plan->column_index_for_class_and_property_before_object_num($actual_prop_meta->class_name, $actual_prop_meta->property_name, $this_object_num); } if (defined $column_num) { $missing_values{$prop_name} = \$column_num; } else { Carp::croak("Can't determine resultset column for $source_class property $source_prop_name for rule $rule"); } } if ($join->{'where'}) { for (my $i = 0; $i < @{$join->{'where'}}; $i += 2) { my $where_prop = $join->{'where'}->[$i]; push @template_filter_names, $where_prop; push @missing_prop_names, $where_prop; my $pos = index($where_prop, ' '); if ($pos != -1) { # the key is "propname op" $where_prop = substr($where_prop,0,$pos); } my $where_value = $join->{'where'}->[$i+1]; $template_filter_values{$where_prop} = $where_value; $missing_values{$where_prop} = $where_value; } } my $missing_rule_tmpl = UR::BoolExpr::Template->resolve($join->{'foreign_class'}, @missing_prop_names)->get_normalized_template_equivalent; my $related_rule_tmpl = UR::BoolExpr::Template->resolve($join->{'foreign_class'}, @template_filter_names)->get_normalized_template_equivalent; my(@hints_or_delegation, @delegations_with_no_objects); # Items in the first listref can be one of three things: # 1) a reference to an integer - meaning retrieve the value from this column in the result set # 2) a reference to a string - meaning retrieve the value from the object usign this as a property name # 3) a string - meaning this is a literal value to fill in directly # The second item is a rule template we'll be feeding these values in to my @template_filter_values = @template_filter_values{$related_rule_tmpl->_property_names}; push @hints_or_delegation, [ \@template_filter_values, $related_rule_tmpl]; my @missing_values = @missing_values{$missing_rule_tmpl->_property_names}; push @delegations_with_no_objects, [\@missing_values, $missing_rule_tmpl]; return (\@hints_or_delegation, \@delegations_with_no_objects); } sub all_object_fabricators { return values %all_object_fabricators; } # simple accessors sub fabricator { my $self = shift; return $self->{'fabricator'}; } sub context { my $self = shift; return $self->{'context'}; } sub all_params_loaded { my $self = shift; return $self->{'all_params_loaded'}; } sub in_clause_values { my $self = shift; return $self->{'in_clause_values'}; } # call the object fabricator closure sub fabricate { my $self = shift; &{$self->{'fabricator'}}; } # Returns true if this fabricator has loaded an object matching this boolexpr sub is_loading_in_progress_for_boolexpr { my $self = shift; my $boolexpr = shift; my $template_id = $boolexpr->template_id; # FIXME should it use is_subsest_of here? return unless exists $self->{'all_params_loaded'}->{$template_id}; return unless exists $self->{'all_params_loaded'}->{$template_id}->{$boolexpr->id}; return 1; } # UR::Contect::_abandon_object calls this to forget about loading an object sub delete_from_all_params_loaded { my($self,$template_id,$boolexpr_id) = @_; return unless ($template_id and $boolexpr_id); my $all_params_loaded = $self->all_params_loaded; return unless $all_params_loaded; return unless exists($all_params_loaded->{$template_id}); delete $all_params_loaded->{$template_id}->{$boolexpr_id}; } sub finalize { my $self = shift; $self->apply_all_params_loaded(); delete $all_object_fabricators{$self}; $self->{'all_params_loaded'} = undef; } sub apply_all_params_loaded { my $self = shift; my $local_all_params_loaded = $self->{'all_params_loaded'}; my @template_ids = keys %$local_all_params_loaded; foreach my $template_id ( @template_ids ) { my @rule_ids = keys %{$local_all_params_loaded->{$template_id}}; foreach my $rule_id ( @rule_ids ) { my $val = $local_all_params_loaded->{$template_id}->{$rule_id}; next unless exists $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; # Has unload() removed this one earlier? $UR::Context::all_params_loaded->{$template_id}->{$rule_id} += $val; } } # Anything left in here is in-clause values that matched nothing. Make a note in # all_params_loaded showing that so later queries for those values won't hit the # data source my $in_clause_values = $self->{'in_clause_values'}; my @properties = keys %$in_clause_values; foreach my $property ( @properties ) { my @values = keys %{$in_clause_values->{$property}}; foreach my $value ( @values ) { my $data = $in_clause_values->{$property}->{$value}; $UR::Context::all_params_loaded->{$data->[0]}->{$data->[1]} = 0; } } $self->{'all_params_loaded'} = {}; } sub DESTROY { my $self = shift; # Don't apply the changes. Maybe the importer closure just went out of scope before # it read all the data my $local_all_params_loaded = $self->{'all_params_loaded'}; if ($local_all_params_loaded) { # finalize wasn't called on this iterator; maybe the importer closure went out # of scope before it read all the data. # Conditionally apply the changes from the local all_params_loaded. If the Context's # all_params_loaded is defined, then another query has successfully run to # completion, and we should add our data to it. Otherwise, we're the only query like # this and all_params_loaded should be cleaned out foreach my $template_id ( keys %$local_all_params_loaded ) { while(1) { my($rule_id, $val) = each %{$local_all_params_loaded->{$template_id}}; last unless $rule_id; if (defined $UR::Context::all_params_loaded->{$template_id}->{$rule_id}) { $UR::Context::all_params_loaded->{$template_id}->{$rule_id} += $val; } else { delete $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; } } } } delete $all_object_fabricators{$self}; } 1; =pod =head1 NAME UR::Context::ObjectFabricator - Track closures used to fabricate objects from data sources =head1 DESCRIPTION Object Fabricators are closures that accept listrefs of data returned by data source iterators, take slices out of them, and construct UR objects out of the results. They also handle updating the query cache and merging changed DB data with previously cached objects. UR::Context::ObjectFabricator objects are used internally by UR::Context, and not intended to be used directly. =head1 METHODS =over 4 =item create_for_loading_template my $fab = UR::Context::ObjectFabricator->create_for_loading_template( $context, $loading_tmpl_hashref, $template_data, $rule, $rule_template, $values, $dsx); Returns an object fabricator instance that is able to construct objects of the rule's target class from rows of data returned by data source iterators. Object fabricators are used a part of the object loading process, and are called by UR::Context::get_objects_for_class_and_rule() to transform a row of data returned by a data source iterator into a UR object. For each class involved in a get request, the system prepares a loading template that describes which columns of the data source data are to be used to construct an instance of that class. For example, in the case where a get() is done on a child class, and the parent and child classes store data in separate tables linked by a relation-property/foreign-key, then the query against the data source will involve and SQL join (for RDBMS data sources). That join will produce a result set that includes data from both tables. The C<$loading_tmpl_hashref> will have information about which columns of that result set map to which properties of each involved class. The heart of the fabricator closure is a list slice extracting the data for that class and assigning it to a hash slice of properties to fill in the initial object data for its class. The remainder of the closure is bookkeeping to keep the object cache ($UR::Context::all_objects_loaded) and query cache ($UR::Context::all_params_loaded) consistent. The interaction of the object fabricator, the query cache, object cache pruner and object loading iterators that may or may not have loaded all their data requires that the object fabricators keep a list of changes they plan to make to the query cache instead of applying them directly. When the Underlying Context Loading iterator has loaded the last row from the Data Source Iterator, it calls C on the object fabricator to tell it to go ahead and apply its changes; essentially treating that data as a transaction. =item all_object_fabricators my @fabs = UR::Context::ObjectFabricator->all_object_fabricators(); Returns a list of all object fabricators that have not yet been finalized =item fabricate my $ur_object = $fab->fabricate([columns,from,data,source]); Given a listref of data pulled from a data source iterator, it slices out the appropriate columns from the list and constructs a single object to return. =item is_loading_in_progress_for_boolexpr my $bool = $fab->is_loading_in_progress_for_boolexpr($boolexpr); Given a UR::BoolExpr instance, it returns true if the given fabricator is prepared to construct objects matching this boolexpr. This is used by UR::Context to know if other iterators are still pulling in objects that could match another iterator's boolexpr, and it should therefore not trust that the object cache is conplete. =item finalize $fab->finalize(); Indicates to the iterator that the caller is done using it for constructing objects, probably because the data source has no more data or the iterator that was using this fabricator has gone out of scope. =item apply_all_params_loaded $fab->apply_all_params_loaded(); As the fabricator constructs objects, it buffers changes to all_params_loaded (the Context's query cache) to maintain consistency if multiple iterators are working concurrently. At the appripriate time, call apply_all_params_loaded() to take those changes and apply them to the current Context's all_params_loaded. =back =cut Process.pm100664023532023421 3024512544604516 16355 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::Process; =pod =head1 NAME UR::Context::Process - Impliments a generic interface to the current application. =head1 SYNOPSIS $name = UR::Context::Process->base_name; $name = UR::Context::Process->prog_name; UR::Context::Process->prog_name($name); $name = UR::Context::Process->pkg_name; UR::Context::Process->pkg_name($name); $name = UR::Context::Process->title; UR::Context::Process->title($name); $version = UR::Context::Process->version; UR::Context::Process->version($version); $author = UR::Context::Process->author; UR::Context::Process->author($author); $author_email = UR::Context::Process->author_email; UR::Context::Process->author_email($author_email); $support_email = UR::Context::Process->support_email; UR::Context::Process->support_email($support_email); $login = UR::Context::Process->real_user_name; =head1 DESCRIPTION This module provides methods to set and retrieve various names associated with the program and the program version number. =cut package UR::Context::Process; our $VERSION = "0.44"; # UR $VERSION;; require 5.006_000; use strict; use warnings; use Sys::Hostname; use File::Basename; require UR; UR::Object::Type->define( class_name => 'UR::Context::Process', is => ['UR::Context'], is_transactional => 0, has => [ host_name => { is => 'Text' }, process_id => { is => 'Integer' }, access_level => { is => 'Text', default_value => '??' }, debug_level => { is => 'Integer', default_value => 0 }, ], doc => 'A context for a given process.', ); =pod =head1 METHODS These methods provide the accessor and set methods for various names associated with an application. =over =item get_current $ctx = UR::Context::Process->get_current(); This is the context which represents the current process. Also available as UR::Context->get_process(); =back =cut sub get_current { return $UR::Context::process; } =pod =over =item has_changes() $bool = UR::Context::Process->has_changes(); Returns true if the current process has changes which might be committed back to the underlying context. =back =cut sub has_changes { my $self = shift; my @ns = $self->all_objects_loaded('UR::Namespace'); for my $ns (@ns) { my @ds = $ns->get_data_sources(); for my $ds (@ds) { return 1 if $ds->has_changes_in_base_context(); } } return; } =pod =over =item _create_for_current_process $ctx = UR::Context::Process->_create_for_current_process(@PARAMS) This is only used internally by UR. It materializes a new object to represent a real process somewhere. TODO: Remove the exception from create(), and allow other processes to be created explicitly w/ the appropriate characteristics. =back =cut sub _create_for_current_process { my $class = shift; die "Process object for the current process already exists!" if $UR::Context::process; #my $rule = $class->define_boolexpr(@_); my $rule = UR::BoolExpr->resolve($class, @_); my $host_name = Sys::Hostname::hostname(); my $id = $host_name . "\t" . $$; my $self = $class->SUPER::create(id => $id, process_id => $$, host_name => $host_name, $rule->params_list); return $self; } sub create { # Note that the above method does creation by going straight to SUPER::create() # for the current process only. die "Creation of parallel/child processes not yet supported!" } # TODO: the remaining methods are from the old App::Name module. # They currently only work for the current process, and operate as class methods. # They should be re-written to work as class methods on $this_process, or # instance methods on any process. For now, only the class methods are needed. =pod =over =item base_name $name = UR::Context::Process->base_name; This is C. =back =cut our $base_name = basename($0, '.pl'); sub base_name { return $base_name } =pod =over =item prog_name $name = UR::Context::Process->prog_name; UR::Context::Process->prog_name($name); This method is used to access and set the name of the program name. This name is used in the output of the C and C subroutines (see L<"version"> and L<"usage">). If given an argument, this method sets the program name and returns the new name or C if unsuccessful. It defaults to C if unspecified. =back =cut our $prog_name; sub prog_name { my $class = shift; my ($name) = @_; if (@_) { $prog_name = $name; } return $prog_name || $class->base_name; } =pod =over =item pkg_name $name = UR::Context::Process->pkg_name; UR::Context::Process->pkg_name($name); This method is used to access and set the GNU-standard package name for the package to which this program belongs. This is does B refer-to a Perl package. It allows a set of spefic programs to be grouped together under a common name, which is used in standard message output, and is used in the output of the C subroutine (see L<"version"> output. If given an argument, this method sets the package name and returns the the new name or C if unsuccessful. Without an argument, the current package name is returned. It defaults to C when unspecified, which in turn defaults to C, which in turn defaults to C. =back =cut # NOTE: this should not use App::Debug because App::Debug::level calls it our $pkg_name; sub pkg_name { my $class = shift; my ($name) = @_; if (@_) { $pkg_name = $name; } return $pkg_name || $class->prog_name; } =pod =over =item title $name = UR::Context::Process->title; UR::Context::Process->title($name); This gets and sets the "friendly name" for an application. It is often mixed-case, with spaces, and is used in autogenerated documentation, and sometimes as a header in generic GUI components. Without an argument, it returns the current title. If an argument is specified, this method sets the application title and returns the new title or C if unsuccessful. It defaults to C when otherwise unspecified, which in turn defaults to C when unspecified, which in turn defaults to C when unspecified, which defaults to C when unspecified. =back =cut our $title; sub title { my $class = shift; my ($name) = @_; if (@_) { $title = $name; } return $title || $class->pkg_name; } =pod =over =item version $version = UR::Context::Process->version; UR::Context::Process->version($version); This method is used to access and set the package version. This version is used in the output of the C method (see L). If given an argument, this method sets the package version and returns the version or C if unsuccessful. Without an argument, the current package version is returned. This message defaults to C<$main::VERSION> if not set. Note that C<$main::VERSION> may be C. =back =cut # set/get version # use $main::VERSION for compatibility with non-App animals. sub version { my $class = shift; my ($version) = @_; if (@_) { $main::VERSION = $version; } return $main::VERSION; } =pod =over =item author $author = UR::Context::Process->author; UR::Context::Process->author($author); This method is used to access and set the package author. If given an argument, this method sets the package author and returns the author or C if unsuccessful. Without an argument, the current author is returned. =back =cut # set/get author our $author; sub author { my $class = shift; my ($name) = @_; if (@_) { $author = $name; } return $author; } =pod =over =item author_email $author_email = UR::Context::Process->author_email; UR::Context::Process->author_email($author_email); This method is used to access and set the package author's email address. This information is used in the output of the C method (see L). If given an argument, this method sets the package author's email address and returns email address or C if unsuccessful. Without an argument, the current email address is returned. =back =cut # set/return author email address our $author_email; sub author_email { my $class = shift; my ($email) = @_; if (@_) { $author_email = $email; } return $author_email; } =pod =over =item support_email $support_email = UR::Context::Process->support_email; UR::Context::Process->support_email($support_email); This method is used to access and set the email address to which the user should go for support. This information is used in the output of the C method (see L). If given an argument, this method sets the support email address and returns that email address or C if unsuccessful. Without an argument, the current email address is returned. =back =cut # set/return author email address our $support_email; sub support_email { my $class = shift; my ($email) = @_; if (@_) { $support_email = $email; } return $support_email; } =pod =over =item real_user_name $login = UR::Context::Process->real_user_name; This method is used to get the login name of the effective user id of the running script. =back =cut # return the name of the user running the program our $real_user_name; sub real_user_name { my $class = shift; if (!$real_user_name) { if ($^O eq 'MSWin32' || $^O eq 'cygwin') { $real_user_name = 'WindowsUser'; } else { $real_user_name = getpwuid($<) || getlogin || 'unknown'; } } return $real_user_name; } =pod =over =item fork $pid = UR::Context::Process->fork; Safe fork() wrapper. Handles properly disconnecting database handles if necessary so that data sources in children are still valid. Also ensures that the active UR::Context::process has the child's PID recorded within. =back =cut sub fork { my $class = shift; my @ds = UR::DataSource->is_loaded(); for (grep {defined $_} @ds) { $_->prepare_for_fork; } my $pid = fork(); unless(defined $pid) { Carp::confess('Failed to fork process. ' . $!); } if (!$pid) { $UR::Context::process = undef; $UR::Context::process = $class->_create_for_current_process; for (grep {defined $_} @ds) { $_->do_after_fork_in_child; } } for (grep {defined $_} @ds) { $_->finish_up_after_fork; } return $pid; } =pod =over =item effective_user_name $login = UR::Context::Process->effective_user_name; This method is used to get the login name of the effective user id of the running script. =back =cut # return the name of the user running the program our $effective_user_name; sub effective_user_name { my $class = shift; if (!$effective_user_name) { $effective_user_name = getpwuid($>) || 'unknown'; } return $effective_user_name; } =pod =over =item original_program_path $path = UR::Context::Process->original_program_path; This method is used to (try to) get the original program path of the running script. This will not change even if the current working directory is changed. (In truth it will find the path at the time UR::Context::Process was used. So, a chdir before that happens will cause incorrect results; in that case, undef will be returned. =back =cut our ($original_program_name, $original_program_dir); eval ' use FindBin; $original_program_dir=$FindBin::Bin; $original_program_name=__PACKAGE__->base_name; '; sub original_program_path { my $class=shift; my $original_program_dir=$class->original_program_dir; return unless($original_program_dir); my $original_program_name=$class->original_program_name; return unless($original_program_name); return $original_program_dir.q(/).$original_program_name; } sub original_program_dir { return unless($original_program_dir); return $original_program_dir; } sub original_program_name { return unless($original_program_name); return $original_program_name; } 1; __END__ =pod =head1 SEE ALSO L =cut 1; Root.pm100664023532023421 472312544604516 15644 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::Root; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Context::Root', is => ['UR::Singleton', 'UR::Context'], is_abstract => 1, is_transactional => 1, doc => 'A base level context, representing the committed state of datasources external to the application.', ); # this is called automatically by UR.pm at the end of the module my $initialized = 0; sub _initialize_for_current_process { my $class = shift; if ($initialized) { die "Attempt to re-initialize the current process?"; } my $context_singleton_class = $ENV{UR_CONTEXT_ROOT} ||= 'UR::Context::DefaultRoot'; $class->set_current($context_singleton_class); } sub name { my $class = shift->_singleton_class_name; my ($name) = ($class =~ /^\w+?\:\:\w+?\:\:(\w+)$/); die "failed to parse name from $class!" unless $name; return lc($name); } sub get_current { #shift->_initialize_for_current_process() unless $initialized; #eval "sub get_current { \$ENV{UR_CONTEXT_ROOT} }"; return $ENV{UR_CONTEXT_ROOT}; } sub set_current { my $class = shift; my $value = shift; return $value if $value eq $ENV{UR_CONTEXT_ROOT}; $ENV{UR_CONTEXT_ROOT} = $value; #print "base context set to $value\n"; #print Carp::longmess(); eval { local $SIG{__DIE__}; local $SIG{__WARN__}; $ENV{UR_CONTEXT_ROOT}->class; }; if ($@) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This failed to compile:\n$@" } unless ($ENV{UR_CONTEXT_ROOT}->isa("UR::Context")) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This does not inherit from UR::Context." } unless ($ENV{UR_CONTEXT_ROOT}->__meta__) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This is not defined with UR::Object::Type metadata!" } # Initialize the bottom of the transaction stack if (@UR::Context::Transaction::open_transaction_stack > 1) { die "Cannot change the base context once transactions are in progress!" } return $value; } sub access_level { my $self = shift->_singleton_object; return "???"; } # sub has_changes { return } 1; Transaction.pm100664023532023421 3152412544604516 17225 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Contextpackage UR::Context::Transaction; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use Carp qw(croak confess shortmess); UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::Context'], has => [ begin_point => { is => 'Integer' }, end_point => { is => 'Integer', is_optional => 1}, # FIXME is this ever used anywhere? state => { is => 'Text' }, # open, committed, rolled-back ], is_transactional => 1, ); our $log_all_changes = 0; our @change_log; our @open_transaction_stack; our $last_transaction_id = 0; sub delete { my $self = shift; $self->rollback; } sub begin { my $class = shift; my $id = $last_transaction_id++; my $begin_point = @change_log; $log_all_changes = 1; my $last_trans = $open_transaction_stack[-1]; if ($last_trans and $last_trans != $UR::Context::current) { die "Current transaction does not match the top of the transaction stack!?" } $last_trans ||= $UR::Context::current; my $self = $class->create( id => $id, begin_point => $begin_point, state => "open", parent => $last_trans, @_ ); unless ($self) { Carp::confess("Failed to being transaction!"); } push @open_transaction_stack, $self; $UR::Context::current = $self; return $self; } sub log_change { my $this_class = shift; my ($object, $class, $id, $aspect, $undo_data) = @_; return if $class eq "UR::Change"; # wrappers (create/delete/load/unload/define) signal change also # and we undo the wrapper, thereby undoing these # -> ignore any signal from a method which is wrapped by another signalling method which gets undone return if ( $aspect eq "load" or $aspect eq "load_external" ); if (!ref($object) or $class eq "UR::Object::Index") { #print "skipping @_\n"; return; } if ($aspect eq "delete") { $undo_data = Data::Dumper::Dumper($object); } Carp::confess() if ref($class); my $change = UR::Change->create( id => scalar(@change_log)+1, changed_class_name => $class, changed_id => $id, changed_aspect => $aspect, undo_data => $undo_data, ); unless (ref($change)) { #$DB::single = 1; } push @change_log, $change; return $change; } sub has_changes { my $self = shift; my @changes = $self->get_changes(); return (@changes > 1 ? 1 : ()); } sub get_changes { my $self = shift; my $begin_point = $self->begin_point; my $end_point = $self->end_point || $#change_log; my @changes = @change_log[$begin_point..$end_point]; if (@_) { @changes = UR::Change->get(id => \@changes, @_) } else { return @changes; } } sub get_change_summary { # TODO: This should compress multiple changes to the same object as much as possible # Right now, it just omits the creation event for the transaction object itself. # -> should the creation of the transaction be part of it? # A: It should really be part of the prior transaction, and after commit/rollback # the nesting collapses. The @change_log should be _inside the transaction object, # or the change should contain a transaction id. The list can be destroyed on # rollback, or summarized on commit. my $self = shift; my @changes = grep { $_->changed_aspect !~ /^(load|define)$/ } $self->get_changes; shift @changes; # $self creation event return @changes; } sub rollback { my $self = shift; # Support calling as a class method: UR::Context::Transaction->rollback rolls back the current trans unless (ref($self)) { $self = $open_transaction_stack[-1]; unless ($self) { Carp::confess("No open transaction!? Cannot rollback."); } } if ($self->state ne "open") { Carp::confess("Cannot rollback a transaction that is " . $self->state . ".") } $self->__signal_change__('prerollback'); my $begin_point = $self->begin_point; unless ($self eq $open_transaction_stack[-1]) { # This is not the top transaction on the stack. # Rollback internally nested transactions in order from the end. my @transactions_with_begin_point = map { [ $_->begin_point, $_ ] } $self->class->get( begin_point => { operator => ">", value => $begin_point } ); my @later_transactions = map { $_->[1] } sort { $b->[0] <=> $a->[0] } @transactions_with_begin_point; for my $later_transaction (@later_transactions) { if ($later_transaction->isa("UR::DeletedRef")) { #$DB::single = 1; } $later_transaction->rollback; } } my $parent = $self->parent; if ($open_transaction_stack[-2] and $open_transaction_stack[-2] != $parent) { die "Parent transaction $parent is not below this one on the stack $open_transaction_stack[-2]?"; } { # Reverse each change, starting from the most recent, and # ending with the creation of the transaction object itself. local $log_all_changes = 0; $self->__signal_change__('rollback', 1); my @changes_to_undo = reverse $self->get_changes(); my $transaction_change = pop @changes_to_undo; my $transaction = $transaction_change->changed_class_name->get($transaction_change->changed_id); unless ($self == $transaction && $transaction_change->changed_aspect eq 'create') { die "First change was not the creation of this transaction!"; } for my $change (@changes_to_undo) { if ($change == $changes_to_undo[0]) { # the transaction reverses itself in its own context, # but the removal of the transaction itself happens in the parent context $UR::Context::current = $parent; } $change->undo; $change->delete; } for my $change (@changes_to_undo) { unless($change->isa('UR::DeletedRef')) { Carp::confess("Failed to undo a change during transaction rollback."); } } $transaction_change->undo; $transaction_change->delete; } $#change_log = $begin_point-1; unless($self->isa("UR::DeletedRef")) { #$DB::single = 1; Carp::confess("Failed to remove transaction during rollback."); } pop @open_transaction_stack; unless (@open_transaction_stack) { $log_all_changes = 0; } $UR::Context::current = $parent; return 1; } sub commit { my $self = shift; # Support calling as a class method: UR::Context::Transaction->commit commits the current transaction. unless (ref($self)) { $self = $open_transaction_stack[-1]; unless ($self) { Carp::confess("No open transaction!? Cannot commit."); } } if ($self->state ne "open") { Carp::confess("Cannot commit a transaction that is " . $self->state . ".") } unless ($open_transaction_stack[-1] == $self) { # TODO: decide if this should work like rollback, and commit nested transactions automatically Carp::confess("Cannot commit a transaction with open sub-transactions!"); } $self->__signal_change__('precommit'); unless ($self->changes_can_be_saved) { return; } $self->state("committed"); if ($self->state eq 'committed') { $self->__signal_change__('commit',1); } else { $self->__signal_change__('commit',0); } pop @open_transaction_stack; unless (@open_transaction_stack) { $log_all_changes = 0; } $UR::Context::current = $self->parent; return 1; } sub changes_can_be_saved { my $self = shift; # This is very similar to behavior in UR::Context::_sync_databases. The only # reason it isn't re-used from UR::Context is the desire to limit changed # objects to those changed within the transaction. # TODO: limit to objects that changed within transaction as to not duplicate # error checking unnecessarily. my @changed_objects = grep { ! $_->isa('UR::DeletedRef') } map { $_->changed_object() } $self->get_changes(); # This is primarily to catch custom validity logic in class overrides. my @invalid = grep { $_->__errors__ } @changed_objects; if (@invalid) { $self->display_invalid_data_for_save(\@invalid); return; } return 1; } sub eval_or_do { my $is_failure = shift; my $block = shift; my $class = __PACKAGE__; if (@_) { confess('%s::eval takes one argument', $class); } my $tx = $class->begin(); my $result = CORE::eval { $block->() }; my $eval_error = $@; if ($is_failure->($result, $eval_error)) { $class->debug_message(shortmess('Rolling back transaction')); $class->debug_message($eval_error) if ($eval_error); unless($tx->rollback()) { die 'failed to rollback transaction'; } } else { unless($tx->commit()) { die 'failed to commit transaction'; } } if (wantarray) { return ($result, $eval_error); } else { return $result; } } # eval function takes a block (&) sort of like CORE::eval # eval will rollback on a caught die sub eval(&) { my $is_failure = sub { my ($result, $eval_error) = @_; return $eval_error; }; return eval_or_do($is_failure, @_); } # do function takes a block (&) sort of like CORE::do # do will rollback on a false result as well as before re-throwing a caught die sub do(&) { my $is_failure = sub { my ($result, $eval_error) = @_; return !$result || $eval_error; }; my ($result, $eval_error) = eval_or_do($is_failure, @_); if ($eval_error) { croak $eval_error, "\t...propogated"; } return $result; } 1; =pod =head1 NAME UR::Context::Transaction - API for software transactions =head1 SYNOPSIS my $o = Some::Obj->create(foo => 1); print "o's foo is ",$o->foo,"\n"; # prints 1 my $t = UR::Context::Transaction->begin(); $o->foo(4); print "In transaction, o's foo is ",$o->foo,"\n"; # prints 4 if (&should_we_commit()) { $t->commit(); print "Transaction committed, o's foo is ",$o->foo,"\n"; # prints 4 } else { $t->rollback(); print "Transaction rollback, o's foo is ",$o->foo,"\n"; # prints 1 } =head1 DESCRIPTION UR::Context::Transaction instances represent in-memory transactions as a diff of the contents of the object cache in the Process context. Transactions are nestable. Their instances exist in the object cache and are subject to the same scoping rules as other UR-based objects, meaning that they do not disappear mearly because the lexical variable they're assigned to goes out of scope. They must be explicitly disposed of via the commit or rollback methods. =head1 INHERITANCE UR::Context::Transaction is a subclass of UR::Context =head1 CONSTRUCTOR =over 4 =item begin $t = UR::Context::Transaction->begin(); Creates a new software transaction context to track changes to UR-based objects. As all activity to objects occurs in some kind of transaction context, the newly created transaction exists within whatever context was current before the call to begin(). =back =head1 METHODS =over 4 =item commit $t->commit(); Causes all objects with changes to save those changes back to the underlying context. =item rollback $t->rollback(); Causes all objects with changes to have those changes reverted to their state when the transaction began. Classes with properties whose meta-property is_transactional => 0 are not tracked within a transaction and will not be reverted. =item delete $t->delete(); delete() is a synomym for rollback =item has_changes $bool = $t->has_changes(); Returns true if any UR-based objects have changes within the transaction. =item get_changes @changes = $t->get_changes(); Return a list or L objects representing changes within the transaction. =back =head1 CLASS METHODS =over 4 =item eval UR::Context::Transaction::eval BLOCK Executes the BLOCK (with no arguments) wrapped by a software transaction and a CORE::eval. If the BLOCK dies then the exception is caught and the software transaction is rolled back. =item do UR::Context::Transaction::do BLOCK Executes the BLOCK (with no arguments) wrapped by a software transaction and a CORE::eval. If the BLOCK returns a true value and does not die then the software transaction is committed. If the BLOCK returns false or dies then the software transaction is rolled back. If the BLOCK throws an exception, it will be caught, the software transaction rolled back, and the exception will be re-thrown with die(). =back =head1 SEE ALSO L =cut DBI.pm100664023532023421 7062412544604516 13716 0ustar00abrummetgsc000000000000UR-0.44/lib/UR# Additional methods for DBI. package UR::DBI; =pod =head1 NAME UR::DBI - methods for interacting with a database. =head1 SYNOPSIS ##- use UR::DBI; UR::DBI->monitor_sql(1); my $dbh = UR::DBI->connect(...); =head1 DESCRIPTION This module subclasses DBI, and provides a few extra methods useful when using a database. =head1 METHODS =over 4 =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; # set up module use base qw(Exporter DBI); our (@EXPORT, @EXPORT_OK); @EXPORT = qw(); @EXPORT_OK = qw(); use IO::Handle; use IO::File; use Time::HiRes; # do not use UR::ModuleBase as base class because it does not play nice with DBI # # UR::DBI control flags # # Build a few class methods to manipulate the environment variables # that control SQL monitoring my %sub_env_map = ( monitor_sql => 'UR_DBI_MONITOR_SQL', monitor_dml => 'UR_DBI_MONITOR_DML', explain_sql_if => 'UR_DBI_EXPLAIN_SQL_IF', explain_sql_slow => 'UR_DBI_EXPLAIN_SQL_SLOW', explain_sql_match => 'UR_DBI_EXPLAIN_SQL_MATCH', explain_sql_callstack => 'UR_DBI_EXPLAIN_SQL_CALLSTACK', no_commit => 'UR_DBI_NO_COMMIT', monitor_every_fetch => 'UR_DBI_MONITOR_EVERY_FETCH', dump_stack_on_connect => 'UR_DBI_DUMP_STACK_ON_CONNECT', ); our ($monitor_sql,$monitor_dml,$no_commit,$monitor_every_fetch,$dump_stack_on_connect, $explain_sql_slow,$explain_sql_if,$explain_sql_match,$explain_sql_callstack); while ( my($subname, $envname) = each ( %sub_env_map ) ) { no strict 'refs'; # There's a scalar of the same name as the sub to hold the value, hook them together *{$subname} = \$ENV{$envname}; my $subref = sub { if (@_ > 1) { $$subname = $_[1]; } return $$subname; }; if ($subname =~ /explain/) { eval "\$$subname = '' if not defined \$$subname"; } else { eval "\$$subname = 0 if not defined \$$subname"; } die $@ if $@; *$subname = $subref; } # by default, monitored SQL goes to STDOUT # FIXME change this 'our' back to a 'my' after we're transisitioned off of the old App API our $sql_fh = IO::Handle->new; $sql_fh->fdopen(fileno(STDERR), 'w'); $sql_fh->autoflush(1); sub sql_fh { $sql_fh = $_[1] if @_ > 1; return $sql_fh; } # # Logging methods # our $log_file; sub log_file { $log_file = pop if @_ > 1; return $log_file; } our $log_fh; my $create_time=0; sub start_logging { return 1 if(defined($log_fh)); return 0 if(-e "$log_file"); $log_fh = new IO::File("> ${log_file}"); unless(defined($log_fh)) { warn "Logging File $log_file Could not be created\n"; return 0; } $create_time=Time::HiRes::time(); return 1; } sub stop_logging { return 1 unless(defined($log_fh)); $log_fh->close; undef $log_fh; } sub log_sql { return 1 unless(defined($log_fh)); my $sql=pop; my $no_timestamp=pop; print $log_fh '=' x 10, "\n" unless($no_timestamp); print $log_fh Time::HiRes::time()-$create_time, "\n" unless($no_timestamp); print $log_fh $sql; } # # Standard DBI overrides # sub connect { my $self = shift; my @params = @_; if ($monitor_sql or $dump_stack_on_connect) { my $time = time; my $time_string = join(' ', $time, '[' . localtime($time) . ']'); $sql_fh->print("DB CONNECT AT: $time_string"); } if ($dump_stack_on_connect) { $sql_fh->print(Carp::longmess()); } $params[2] = 'xxx'; # Param 3 is usually a hashref of connection modifiers if (ref($params[3]) and ref($params[3]) =~ m/HASH/) { my $string = join(', ', map { $_ . ' => ' . $params[3]->{$_} } keys(%{$params[3]}) ); $params[3] = "{ $string }"; } my $params_stringified = join(",", map { defined($_) ? "'$_'" : 'undef' } @params); UR::DBI::before_execute("connecting with params: ($params_stringified)"); my $rv = $self->SUPER::connect(@_); UR::DBI::after_execute(); return $rv; } # # UR::Object hooks # sub commit_all_app_db_objects { my $this_class = shift; my $handle = shift; my $data_source; if ($handle->isa("UR::DBI::db")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle); } elsif ($handle->isa("UR::DBI::st")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle->{Database}); } else { Carp::confess("No handle passed to method!?") } unless ($data_source) { return; } return $data_source->_set_all_objects_saved_committed(); } sub rollback_all_app_db_objects { my $this_class = shift; my $handle = shift; my $data_source; if ($handle->isa("UR::DBI::db")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle); } elsif ($handle->isa("UR::DBI::st")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle->{Database}); } else { Carp::confess("No handle passed to method!?") } unless ($data_source) { Carp::confess("No data source found for database handle! $handle") } return $data_source->_set_all_objects_saved_rolled_back(); } my @disable_dump_and_explain; sub _disable_dump_explain { push @disable_dump_and_explain, [$monitor_sql,$explain_sql_slow,$explain_sql_match]; $monitor_sql = 0; $explain_sql_slow = ''; $explain_sql_match = ''; } sub _restore_dump_explain { if (@disable_dump_and_explain) { my $vars = pop @disable_dump_and_explain; ($monitor_sql,$explain_sql_slow,$explain_sql_match) = @$vars; } else { Carp::confess("No state saved for disabled dump/explain"); } } # The before_execute/after_execute subroutine pair # are callbacks called by execute() and by other # methods which implicitly execute a statement. # They use these three varaibles to track state, # presuming that the callback pair cannot be nested. print("\nEXPLAIN QUERY MATCHING /$explain_sql_match/gi" . ($val ne $sql ? " (on value '$val') " : "") ); if ($monitor_sql) { $sql_fh->print("\n"); } else { _print_sql_and_params($sql,@_); } if ($explain_sql_callstack) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } if ($UR::DBI::explained_queries{$sql}) { $sql_fh->print("(query explained above)\n"); } else { UR::DBI::_print_query_plan($sql,$dbh); $UR::DBI::explained_queries{$sql} = 1; } last; } } } my $start_time = _set_start_time(); if ($monitor_sql){ _print_sql_and_params($sql,@_); if ($monitor_sql > 1) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } _print_monitor_label("EXECUTE"); } elsif($monitor_dml && $sql !~ /^\s*select/i){ _print_sql_and_params($sql,@_); _print_monitor_label("EXECUTE"); $monitor_dml=2; } no warnings; UR::DBI::log_sql_for_summary($sql); # $ENV{UR_DBI_SUMMARIZE_SQL} my $log_sql_str = _generate_sql_and_params_log_entry($sql, @_); UR::DBI::log_sql($log_sql_str); return $start_time; } sub after_execute { #my ($sql,@params) = @_; my $elapsed_time = _set_elapsed_time(); if ($monitor_sql){ _print_elapsed_time(); } elsif($monitor_dml == 2){ _print_elapsed_time(); $monitor_dml = 1; } UR::DBI::log_sql(1, ($elapsed_time)."\n"); return $elapsed_time; } # The before_fetch/after_fetch pair are callback # called by fetch() and by other methods which implicitly # fetch data w/o explicitly calling fetch(). our $_fetching = 0; sub before_fetch { my $sth = shift; return if @disable_dump_and_explain; if ($_fetching) { Carp::cluck("before_fetch called after another before_fetch w/o intervening after_fetch!"); } $_fetching = 1; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; if ($monitor_sql) { if ($fetch_timing_arrayref and @$fetch_timing_arrayref == 0) { UR::DBI::_print_monitor_label('FIRST FETCH'); } elsif ($monitor_every_fetch) { UR::DBI::_print_monitor_label('NTH FETCH'); } } return UR::DBI::_set_start_time(); } sub after_fetch { my $sth = shift; return if @disable_dump_and_explain; $_fetching = 0; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; my $time; push @$fetch_timing_arrayref, UR::DBI::_set_elapsed_time(); if ($monitor_sql) { if ($monitor_every_fetch || @$fetch_timing_arrayref == 1) { $time = UR::DBI::_print_elapsed_time(); } } if (@$fetch_timing_arrayref == 1) { my $time = $sth->execute_time + $fetch_timing_arrayref->[0]; UR::DBI::_check_query_timing($sth->{Statement},$time,$sth->{Database},$sth->last_params); } return $time; } sub after_all_fetches_with_sth { my $sth = shift; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; # This arrayref is set when it goes through the subclass' execute(), # and is removed when we finish all fetches(). # Since a variety of things attempt to call this from the various "final" # positions of an $sth we delete this so the final callback operates only once. # Also, internally generated $sths which do not get executed() normally # will be skipped by this check. if (!$fetch_timing_arrayref) { # internal sth which did not go through prepare() #print $sql_fh "SKIP STH\n"; return; } $sth->fetch_timing_arrayref(undef); my $print_fetch_summary; if ($monitor_sql and $sth->{Statement} =~ /select/i) { $print_fetch_summary = 1; UR::DBI::_print_monitor_label('TOTAL EXECUTE-FETCH'); } my $time = $sth->execute_time; if (@$fetch_timing_arrayref) { for my $fetch_time (@$fetch_timing_arrayref ) { $time += $fetch_time; } if ($print_fetch_summary) { UR::DBI::_print_monitor_time($time); } # since there WERE fetches, we already checked query timing } else { if ($print_fetch_summary) { UR::DBI::_print_monitor_time($time); } # since there were NOT fetches, we check query timing now UR::DBI::_check_query_timing($sth->{Statement},$time,$sth->{Database},$sth->last_params); } return $time; } sub after_all_fetches_no_sth { my ($sql, $time, $dbh, @params) = @_; $time = _set_elapsed_time() unless defined $time; if ($monitor_sql and $sql =~ /select/i) { UR::DBI::_print_monitor_label('TOTAL EXECUTE-FETCH'); UR::DBI::_print_monitor_time($time); } # no sth = no fetches = no query timing check done yet... UR::DBI::_check_query_timing($sql,$time,$dbh,@params); return $time; } my $__SQL_SUMMARY__ = {}; sub log_sql_for_summary { my ($sql) = @_; $__SQL_SUMMARY__->{$sql}++; } sub print_sql_summary { for my $sql (sort {$__SQL_SUMMARY__->{$b} <=> $__SQL_SUMMARY__->{$a}} keys %$__SQL_SUMMARY__) { print STDERR join('',"********************\n", $__SQL_SUMMARY__->{$sql}, " instances of query: $sql\n"); } } # These methods are called by the above. sub _generate_sql_and_params_log_entry { my $sql = shift; no warnings; my $sql_log_str = "\nSQL: $sql\n"; if (@_) { $sql_log_str .= "PARAMS: "; $sql_log_str .= join(", ", map { defined($_) ? "'$_'" : "NULL" } map { scalar(grep { $_ } map { 128 & ord $_ } split(//, substr($_, 0, 64))) ? '' : $_ } @_ ) . "\n"; } return $sql_log_str; } sub _print_sql_and_params { my $sql = shift; my $entry = _generate_sql_and_params_log_entry($sql, @_); no warnings; print $sql_fh $entry; } sub _set_start_time { $start_time=&Time::HiRes::time(); } our $_print_monitor_label_or_time_is_ready_for = "label"; sub _print_monitor_label { #Carp::cluck() unless $_print_monitor_label_or_time_is_ready_for eq "label"; my $time_label = shift; $sql_fh->print("$time_label TIME: "); $_print_monitor_label_or_time_is_ready_for = "time"; } sub _print_monitor_time { #Carp::cluck() unless $_print_monitor_label_or_time_is_ready_for eq "time"; $sql_fh->printf( "%.4f s\n", shift); $_print_monitor_label_or_time_is_ready_for = "label"; } sub _set_elapsed_time { $elapsed_time = &Time::HiRes::time()-$start_time; } sub _print_elapsed_time { _print_monitor_time($elapsed_time); } our $_print_check_for_slow_query = 0; sub _check_query_timing { my ($sql,$time,$dbh,@params) = @_; return if @disable_dump_and_explain; return unless $sql =~ /select/i; print $sql_fh "CHECK FOR SLOW QUERY:\n" if $_print_check_for_slow_query; # used only by a test case if (length($explain_sql_slow) and $time >= $explain_sql_slow) { $sql_fh->print("EXPLAIN QUERY SLOWER THAN $explain_sql_slow seconds ($time):"); if ($monitor_sql || ($monitor_dml && $sql !~ /^\s*select/i)) { $sql_fh->print("\n"); } else { _print_sql_and_params($sql,@params); } if ($explain_sql_callstack) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } if ($UR::DBI::explained_queries{$sql}) { $sql_fh->print("(query explained above)\n"); } else { $UR::DBI::explained_queries{$sql} = 1; UR::DBI::_print_query_plan($sql,$dbh); } } } sub _print_query_plan { my ($sql,$dbh,%params) = @_; UR::DBI::_disable_dump_explain(); $dbh->do($UR::DBI::EXPLAIN_PLAN_CLEANUP_DML); # placeholders in explain plan queries on windows # results in Oracle throwing an ORA-00600 error, # likely due to interaction with DBI. Replace with # literals. if ($^O eq "MSWin32" || $^O eq 'cygwin') { $sql =~ s/\?/'1'/g; } $dbh->do($UR::DBI::EXPLAIN_PLAN_DML . "\n" . $sql) or die "Failed to produce query plan! " . $dbh->errstr; UR::DBI::Report->generate( sql => [$UR::DBI::EXPLAIN_PLAN_SQL], dbh => $dbh, count => 0, outfh => $sql_fh, %params, "explain-sql" => 0, "echo" => 0, ); $sql_fh->print("\n"); $dbh->do($UR::DBI::EXPLAIN_PLAN_CLEANUP_DML); UR::DBI::_restore_dump_explain(); return 1; } ############ # # Database handle subclass # ############ package UR::DBI::db; use strict; use warnings; our @ISA = qw(DBI::db); sub commit { my $self = shift; # unless ($no_commit) { # print "\n\n\n************* FORCIBLY SETTING NO-COMMIT FOR TESTING. This would have committeed!!!! **********\n\n\n"; # $no_commit = 1; # } if ($no_commit) { # Respect the ->no_commit(1) setting. UR::DBI::before_execute("commit (ignored)"); UR::DBI::after_execute; return 1; } else { if(UR::DataSource->use_dummy_autogenerated_ids) { # Not cool...you shouldn't have dummy-ids on and no-commit off # Don't commit, and notify the authorities UR::DBI::before_execute("commit (ignored)"); $UR::Context::current->error_message('Tried to commit with dummy-ids on and no-commit off'); UR::DBI::after_execute; #$UR::Context::current->send_email( # To => 'example@example.edu', # Subject => 'attempt to commit with dummy-ids on and no-commit off '. # "by $ENV{USER} on $ENV{HOST} running ". # UR::Context::Process->original_program_path." as pid $$", # Message => "Call stack:\n" .Carp::longmess() #); } else { # Commit and update the associated objects. UR::DBI::before_execute("commit"); my $rv = $self->SUPER::commit(@_); UR::DBI::after_execute; if ($rv) { UR::DBI->commit_all_app_db_objects($self) } return $rv; } } } sub commit_without_object_update { UR::DBI::before_execute("commit (no object updates)"); my $rv = shift->SUPER::commit(@_); UR::DBI::after_execute(); return $rv; } sub rollback { my $self = shift; UR::DBI::before_execute("rollback"); my $rv = $self->SUPER::rollback(@_); UR::DBI::after_execute(); if ($rv) { UR::DBI->rollback_all_app_db_objects($self) } return $rv; } sub rollback_without_object_update { UR::DBI::before_execute("rollback (w/o object updates)"); my $rv = shift->SUPER::commit(@_); UR::DBI::after_execute(); return $rv; } sub disconnect { my $self = shift; # Rollback if AutoCommit is 0. Oracle commits by default on disconnect. # Rolling back when AutoCommit is on will generate a DBI warning. if ($self->{'AutoCommit'} == 0) { $self->rollback; } # Msg and disconnect. UR::DBI::before_execute("disconnecting"); my $rv = $self->SUPER::disconnect(@_); UR::DBI::after_execute(); # There doesn't seem to be anything less which # sets this, but legacy tools did if ( (defined $UR::DBI::common_dbh) and ($self eq $UR::DBI::common_dbh) ) { UR::DBI::before_execute("common dbh removed"); $UR::DBI::common_dbh = undef; UR::DBI::after_execute("common dbh removed"); } return $rv; } sub prepare { my $self = shift; my $sql = $_[0]; my $sth; #print $sql_fh "PREPARE: $sql\n"; if ($sql =~ /^\s*(commit|rollback)\s*$/i) { unless ($sql =~ /^(commit|rollback)$/i) { Carp::confess("Executing a statement with an embedded commit/rollback?\n$sql\n"); } if ($sth = $self->SUPER::prepare(@_)) { if ($1 =~ /commit/i) { $UR::DBI::prepared_commit{$sth} = 1; } elsif ($1 =~ /rollback/) { $UR::DBI::prepared_rollback{$sth} = 1; } } } else { $sth = $self->SUPER::prepare(@_) or return; } return $sth; } # For newer versions of DBI, some of the $dbh->select* methods do not # call execute internally, so SQL dumping and logging will not occur. # These are listed below, and the bad ones are overridden. # selectall_hashref ok # selectcol_arrayref ok # selectrow_hashref ok # selectall_arrayref bad # selectrow_arrayref bad # selectrow_array bad sub selectall_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my $ar = $self->SUPER::selectall_arrayref(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectcol_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); UR::DBI::_disable_dump_explain(); my $ar = $self->SUPER::selectcol_arrayref(@_); UR::DBI::_restore_dump_explain(); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectall_hashref { my $self = shift; my @p = ($_[0],@_[3..$#_]); UR::DBI::before_execute($self,@p); UR::DBI::_disable_dump_explain(); my $ar = $self->SUPER::selectall_hashref(@_); UR::DBI::_restore_dump_explain(); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectrow_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my $ar = $self->SUPER::selectrow_arrayref(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectrow_array { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my @a = $self->SUPER::selectrow_array(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return @a if wantarray; return $a[0]; } sub DESTROY { UR::DBI::before_execute("destroying connection"); shift->SUPER::DESTROY(@_); UR::DBI::after_execute("destroying connection"); } ######### # # Statement handle subclass # ######### package UR::DBI::st; use strict; use warnings; use Time::HiRes; use Sys::Hostname; use Devel::GlobalDestruction; our @ISA = qw(DBI::st); sub _mk_mutator { my ($class, $method) = @_; # Make a more specific key based on the package # to try not to conflict with anything else. # This must start with 'private_'. See DBI docs on subclassing. my $hash_key = join('_', 'private', lc $class, lc $method); $hash_key =~ s/::/_/g; my $sub = sub { return if Devel::GlobalDestruction::in_global_destruction; my $sth = shift; if (@_) { no warnings 'uninitialized'; $sth->{$hash_key} = shift; } no warnings; return $sth->{$hash_key}; }; no strict; *{$class . '::' . $method} = $sub; } for my $method (qw(execute_time fetch_timing_arrayref last_params_arrayref)) { __PACKAGE__->_mk_mutator($method); } sub last_params { my $ret = shift->last_params_arrayref; unless (defined $ret) { $ret = []; } @{ $ret }; } sub execute { my $sth = shift; # (re)-initialize the timing array if (my $a = $sth->fetch_timing_arrayref()) { # re-executing on a previously used $sth. UR::DBI::after_all_fetches_with_sth($sth); } else { # initialize the $sth on first execute. $sth->fetch_timing_arrayref([]); } $sth->last_params_arrayref([@_]); UR::DBI::before_execute($sth->{Database},$sth->{Statement},@_); my $rv = $sth->SUPER::execute(@_); UR::DBI::after_execute($sth->{Database},$sth->{Statement},@_); # record the elapsed time for execution. $sth->execute_time($UR::DBI::elapsed_time); if ($rv) { if (my $prev = $UR::DBI::prepared_commit{$sth}) { UR::DBI->commit_all_app_db_objects($sth); } if (my $prev = $UR::DBI::prepared_rollback{$sth}) { UR::DBI->rollback_all_app_db_objects($sth); } } return $rv; } sub fetchrow_array { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my @a = $sth->SUPER::fetchrow_array(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return @a if wantarray; return $a[0]; } sub fetchrow_arrayref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchrow_arrayref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return $ar; } sub fetchall_arrayref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchall_arrayref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); UR::DBI::after_all_fetches_with_sth($sth,@_); return $ar; } sub fetchall_hashref { my $sth = shift; my @p = @_[1,$#_]; UR::DBI::before_fetch($sth,@p); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchall_hashref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@p); UR::DBI::after_all_fetches_with_sth($sth,@_[1,$#_]); return $ar; } sub fetchrow_hashref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchrow_hashref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return $ar; } sub fetch { my $sth = shift; UR::DBI::before_fetch($sth,@_); my $rv = $sth->SUPER::fetch(@_); UR::DBI::after_fetch($sth,@_); return $rv; } sub finish { my $sth = shift; UR::DBI::after_all_fetches_with_sth($sth); return $sth->SUPER::finish(@_); } sub DESTROY { delete $UR::DBI::prepared_commit{$_[0]}; delete $UR::DBI::prepared_rollback{$_[0]}; #print $sql_fh "DESTROY1\n"; UR::DBI::after_all_fetches_with_sth(@_); # does nothing if called previously by finish() #print $sql_fh "DESTROY2\n"; #Carp::cluck(); shift->SUPER::DESTROY(@_); } $UR::DBI::STATEMENT_ID = $$ . '@' . hostname(); $UR::DBI::EXPLAIN_PLAN_DML = "explain plan set statement_id = '$UR::DBI::STATEMENT_ID' into plan_table for "; $UR::DBI::EXPLAIN_PLAN_SQL = qq/ select LPAD(' ',p.LVL-1) || OPERATION OPERATION, OPTIONS, --(case when p.OBJECT_OWNER is null then '' else p.OBJECT_OWNER || '.' end) -- || p.OBJECT_NAME || (case when p.OBJECT_TYPE is null then '' else ' (' || p.OBJECT_TYPE || ')' end) "OBJECT", (case when i.table_name is not null then i.table_name || '(' || index_column_names || ')' else '' end) "OBJECT_IS_ON", p.COST, p.CARDINALITY CARD, p.BYTES, p.OPTIMIZER, p.CPU_COST CPU, p.IO_COST IO, p.TEMP_SPACE TEMP, i.index_type "index_type", i.last_analyzed "index_analyzed" from ( SELECT plan_table.*, level lvl FROM PLAN_TABLE CONNECT BY prior id = parent_id AND prior statement_id = statement_id START WITH id = 0 AND statement_id = '$UR::DBI::STATEMENT_ID' ) p full join dual on dummy = dummy left join all_indexes i on i.index_name = p.object_name and i.owner = p.object_owner left join ( select index_owner, index_name, LTRIM(MAX(SYS_CONNECT_BY_PATH(ic.column_name,',')) KEEP (DENSE_RANK LAST ORDER BY ic.column_position),',') index_column_names from ( select ic.index_owner, ic.index_name, ic.column_name, ic.column_position from all_ind_columns ic ) ic group by ic.index_owner, ic.index_name connect by index_owner = prior index_owner and index_name = prior index_name and column_position = PRIOR column_position + 1 start with column_position = 1 ) index_columns_stringified on index_columns_stringified.index_owner = i.owner and index_columns_stringified.index_name = i.index_name where p.object_name is not null ORDER BY p.id /; $UR::DBI::EXPLAIN_PLAN_CLEANUP_DML = "delete from plan_table where statement_id = '$UR::DBI::STATEMENT_ID'"; 1; __END__ =pod =back =head1 SEE ALSO UR(3), UR::DataSource::RDBMS(3), UR::Context(3), UR::Object(3) =cut #$Header$ Report.pm100664023532023421 4517012544604516 15167 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DBI =pod =head1 NAME UR::DBI::Report - a database report interface =head1 SYNOPSIS ##- use UR::DBI::Report; UR::DBI::Report->use_standard_cmdline_options(); UR::DBI::Report->generate(sql => \@ARGV); =head1 DESCRIPTION This module is a reporting interface which takes SQL queries in a variety of forms and prints their results with formatting options. =cut use strict; use warnings; package UR::DBI::Report; use base 'UR::ModuleBase'; require UR; our $VERSION = "0.44"; # UR $VERSION; use Data::Dumper; use Time::HiRes; # Support some options right on the "use" line. sub import { my $class = shift; my %params = @_; UR::DBI::Report->extend_command_line() if delete $params{extend_command_line}; die "Unknown options passed-to " . __PACKAGE__ . join(", ", keys %params) if keys %params; } # Applications which do no additional configuration will get these parameters by default. our %module_default_params = ( delimit => 'spaces', header => 1, count => 1, orient => 'vert', trunc => 35, sloppy => 0, nulls => 1, data => 1, combine => 0, "explain-sql" => 0, ); # Applications which call this method before init() will allow the user # to override reporting defaults via standard command line options. our %application_default_params = %module_default_params; sub extend_command_line { # this callback processes all of the options and sets application defaults for this module my $parse_option_callback; $parse_option_callback = sub { my ($flag,$value) = @_; if ($flag eq 'parse') { $parse_option_callback->("header",!$value); $parse_option_callback->("count",!$value); $parse_option_callback->("delimit",($value ? "tabs" : "spaces")); $parse_option_callback->("trunc",($value ? undef : $application_default_params{"trunc"})); return 1; } $application_default_params{$flag} = $value; return 1; }; # ask Getopt to expect some new cmdline parameters UR::Command::Param->add( map { if (ref($_)) { $_->{module} = "Data Report Formatting" } $_; } delimit => { action => $parse_option_callback, msg => "spaces|tabs: spaces separate columns evenly, tab-delimited columns are easiliy parsed", argument => '=s', option => '--delimit', }, header => { action => $parse_option_callback, msg => "Show column headers.", argument => '!', option => '--header', }, data => { action => $parse_option_callback, msg => "Show returned query data (on by default!).", argument => '!', option => '--data', }, count => { action => $parse_option_callback, msg => "Show row count at the end of output.", argument => '!', option => '--count', }, orient => { action => $parse_option_callback, msg => "vert: (default) one row per output line, horiz: one row per output column.", argument => '=s', option => '--orient', }, trunc => { action => $parse_option_callback, msg => "Set column truncation for long values. A zero setting truncates at the level of the default DBI LongReadLen; see DBI documentation for details.", argument => '=s', option => '--trunc', }, sloppy => { action => $parse_option_callback, msg => "When processing multiple SQL statements and a failure occurs, just proceed to the next statement.", argument => '!', option => '--sloppy', }, nulls => { action => $parse_option_callback, msg => "Show nulls. When turned-off with 'no-nulls' replaces them with a ?.", argument => '!', option => '--nulls', }, parse => { action => $parse_option_callback, msg => "Equivalent to --noheader --nocount --tabs --trunc=0", argument => '!', option => '--parse', }, echo => { action => $parse_option_callback, msg => "Print SQL before its first execution. Does not print multiple times on multiple executes with different params.", argument => '!', option => '--echo', }, combine => { action => $parse_option_callback, msg => "When executing the same query multiple times with different params, combine the results as though it were one query.", argument => '!', option => '--combine', }, "explain-sql" => { action => $parse_option_callback, msg => "Dump a query plan instead of running the query.", argument => '!', option => '--explain-sql', } ); } # # This method executes the specified sql statements and prints reports for each. # sub generate { my $class = shift; my %params = (%application_default_params, @_); my $sql_param = delete $params{sql}; my @queries = (ref($sql_param) ? (@$sql_param) : ($sql_param) ); my $dbh = delete $params{dbh}; unless ($dbh) { Carp::confess("No dbh sent to UR::DBI::Report, and no default available anymore!"); } $dbh->{LongTruncOk} = 1; if ($params{trunc}) { $dbh->{LongReadLen} = $params{trunc}; } elsif(defined($params{trunc})) { warn "Setting the trunc value to 0 does not guarantee no truncating."; warn "There is no way to completely prevent truncating in the current version of DBI."; warn "The current trunc limit is $dbh->{LongReadLen}."; warn "If this does not satisfy your needs, try setting trunc to a higher number"; } # The outer loop runs once per SQL statement. my $sql_request; while($sql_request = shift(@queries)) { # The SQL comes from the cmdline or STDIN. my $sql; if ($sql_request eq '-') { $sql = ''; while () { next if (/^#!/); if (/;\s*$/) { s/;\s*$//; $sql .= $_; last; } else { $sql .= $_; } } } else { $sql = $sql_request; } next if ($sql !~ /\S/); chomp($sql); print "SQLRUN: $sql\n" if $params{echo}; # See if we expect paramters from STDIN my $question_marks = $sql; $question_marks =~ s/[^\?]//msg; my $question_mark_count = length($question_marks); if ($params{"explain-sql"}) { my $outfh = IO::Handle->new; $outfh->fdopen(fileno(STDOUT), 'w'); UR::DBI::_print_query_plan($sql,$dbh,outfh => $outfh,%params); # skip past any parameters, since we're not really executing, # and they don't (can't) affect the query plan if ($question_mark_count) { my $data; while (1) { $data = ; chomp $data; last unless (defined($data) and length($data)); } } # redo if we're reading from stdin, otherwise go to the next specified cmd if ($sql_request eq '-') { redo; } else { next; } } # This will never get re-prepared my $sth = $dbh->prepare_cached($sql); unless($sth) { if ($params{sloppy}) { App::UI->error_message($dbh->errstr); next; } else { die $dbh->errstr; } } # This flag may be set after the first parameter set runs to speed further executions. # The inner loop runs once per required execution of the SQL. # SQL is executed multiple times if there are ? placeholders and there are multiple lines on STDIN my ($combine_row_count, $combine_time)=(0, 0); my $sql_execution_count = 0; my $statement_is_not_a_query = 0; my $outfh = $params{outfh}; for (1) { # Get params from STDIN if necessary my @params; if ($question_mark_count) { # Get data from STDIN as needed for any ?s. my $data = ; chomp $data if defined($data); # If we have a ? count and there is no data on this line, we're done with this SQL statement. unless (defined($data) and length($data)) { # We want to warn the user if a SQL statement had no params at all. if ($sql_execution_count == 0) { $class->error_message("No params!"); } # On to the next staement, if there is one. last; } @params = split(/\t/, $data); $#params = $question_mark_count - 1; if ($params{echo}) { print "PARAMS: @params\n"; } } # Note the time so we can show the elapsed time. my $t1 = Time::HiRes::time(); # Execute the current statement with the parameters. my $execcnt; unless ($execcnt = $sth->execute(@params)) { my $msg = "Failed to execute SQL:\n$sql\n" . (@params ? "Data:\n>" . join(",",@params) . "<\n" : '') . $sth->errstr; if ($params{sloppy}) { App::UI->error_message($msg); } else { die $msg; } } # Count these for better error messaging. $sql_execution_count++; # Count results returned (SQL) or affected (DML). my $rowcnt; # This flag may not be set until we try to get the first result. unless ($statement_is_not_a_query) { $rowcnt = UR::DBI::Report->print_formatted( sth => $sth, outfh => $outfh, ( $params{combine} ? (position_in_combined_sql_list => $sql_execution_count) :() ), %params ); $statement_is_not_a_query = 1 if defined($rowcnt) and ($rowcnt eq "0 because the statement is not a query"); } # Flush any data pending to the output filter. if (ref($outfh) and not $params{combine} and not $params{outfh}) { $outfh->close; $outfh = undef; } $sth->finish; # Summarize the effect of the query/dml. if ($params{count}) { if($params{combine}) { #If we're doing a combined output, we'll have to tally these up for later $combine_row_count+=$statement_is_not_a_query?($execcnt+0):($rowcnt+0); $combine_time+=Time::HiRes::time()-$t1; } else { my $td = Time::HiRes::time() - $t1; $td =~ s/(\.\d\d\d).*/$1/; if ($statement_is_not_a_query) { print (($execcnt+0) . " row(s) affected. Execution time: ${td} second(s).\n"); } else { print (($rowcnt+0) . " row(s) returned. Execution time: ${td} second(s).\n"); } } } # By default this block will execute just once. # Continue if there is a question_mark_count. # It will "last" out at the top if there is no more data on stdin. redo if $question_mark_count; } # end params loop if ($params{combine}) { $outfh->close if ref($outfh) and not $params{outfh}; if ($params{count}) { $combine_time=~s/(\.\d\d\d).*/$1/; print("$combine_row_count row(s) ".($statement_is_not_a_query?'affected':'returned'). ". Execution time: $combine_time second(s).\n"); } } # If the cmdline sql was a dash, we're reading from STDIN until it exits the loop. redo if $sql_request eq '-'; } # end SQL loop # Done executing all SQL. return 1; } # end of sqlrun subroutine # This method prints a single report for a given statement handle. sub print_formatted { my $class = shift; my %params = (%application_default_params, @_); # sth A statement handle from which the data comes. # sql If no handle is specified, the SQL to use. # infh If no sth or sql is specified, a handle from which sql can be pulled. # If sth or sql ARE specifed, a handle from which parameter values can be pulled. my $sth = delete $params{sth}; unless ($sth) { } # outfh An optional handle to which the report is written. my $outfh = delete $params{outfh}; if ($outfh) { if ($params{delimit} =~ /^s/i && $^O ne "MSWin32" && $^O ne 'cygwin') { # We only handle one case of $outfh and still do tab2col. # If it's stderr, we redirect there. $outfh = IO::File->new('| tab2col --nocount 1>&' . fileno($outfh)); Carp::confess("Failed to pipe through tab2col!") unless $outfh; } } else { if ($params{delimit} =~ /^s/i) { # Handle tab-delimit via tab2col $outfh = IO::File->new("| tab2col --nocount"); } else { $outfh = IO::Handle->new; $outfh->fdopen(fileno(STDOUT), 'w'); } } # This is the return value. # Set to an integer, or to the false-valued string "0 because the statement is not a query". my $rowcnt = 0; # Get the column names into an array of headers. my @headers = @{ $sth->{NAME_uc} }; # Display as needed according the requested orientation. if ($params{orient} =~ /^v/i) # lines listed vertically { # Get the first row, but re-hook warnings first to see if we # are really running a query wich can return data (not DML). my $msg; local $SIG{__WARN__} = sub { $msg = shift }; my $row = $sth->fetchrow_arrayref; if ($msg =~ /ERROR no statement executing/) { # Set this flag so we do not re-try fetch*() on this query. return "0 because the statement is not a query"; } elsif ($sth->errstr) { die $sth->errstr; } else { if ($params{data}) { # Spacers are dashes. my @spacers = @headers; for (@spacers) { $_ =~ s/./-/g } # Print the headers, a line of spacers, then one line for each result row. if ($params{header} and not ($params{combine} and $params{position_in_combined_sql_list} > 1)) { if (my $trunc = $params{trunc}) { for my $row (\@headers, \@spacers) { print $outfh join("\t", map { substr($_,0,$trunc) } @$row) . "\n"; } } else { for my $row (\@headers, \@spacers) { print $outfh join("\t",@$row) . "\n"; } } } # Print the initial row, and any others we can fetch(). while ($row) { print $outfh join("\t",@$row) . "\n"; $rowcnt++; $row = $sth->fetchrow_arrayref; } } else { # Just get the count while ($row) { $rowcnt++; $row = $sth->fetchrow_arrayref } } } } elsif ($params{orient} =~ /^h/i) { my $msg; local $SIG{__WARN__} = sub { $msg = shift }; my $results = $sth->fetchall_arrayref; if ($msg =~ /ERROR no statement executing/) { # Set this flag so we do not re-try fetch*() on this query. return "0 because the statement is not a query"; } else { # Process the fetched data. $rowcnt = scalar(@$results); if ($params{data}) { # Show the data my $cnum = 0; if (my $trunc = $params{trunc}) { for my $header (@headers) { print $outfh $header . "\t:\t" if ($params{header}); print $outfh join("\t", map { substr($_->[$cnum],0,$trunc) } @$results) . "\n"; $cnum++; } } else { for my $header (@headers) { $outfh->print($header . "\t:\t") if ($params{header}); $outfh->print(join("\t", map { $_->[$cnum] } @$results) . "\n"); $cnum++; } } } } } else { $class->error_message("Unknown orientation $params{orient}"); return; } return $rowcnt; } 1; DataSource.pm100664023532023421 6447012544604516 15354 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::DataSource; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use Sys::Hostname; { no warnings 'once'; *namespace = \&get_namespace; } UR::Object::Type->define( class_name => 'UR::DataSource', is_abstract => 1, doc => 'A logical database, independent of prod/dev/testing considerations or login details.', has => [ namespace => { calculate_from => ['id'] }, is_connected => { is => 'Boolean', default_value => 0, is_optional => 1, is_transient => 1 }, get_default_handle => { is_calculated => 1, is_constant => 1, doc => 'Underlying handle for this datasource', calculate => '$self->create_default_handle_wrapper', }, ], valid_signals => ['precreate_handle', 'create_handle', 'predisconnect_handle', 'disconnect_handle' ], ); our @CARP_NOT = qw(UR::Context UR::DataSource::QueryPlan); sub define { shift->__define__(@_) } sub get_namespace { my $class = shift->class; return substr($class,0,index($class,"::DataSource")); } sub get_name { my $class = shift->class; return lc(substr($class,index($class,"::DataSource")+14)); } # The default used to be to force table/column/constraint/etc names to # upper case when storing them in the MetaDB, and in the column_name # metadata for properties. The new behavior is to just use whatever the # database supplies us when interrogating the data dictionary. # For datasources/clases that still need the old behavior, override this # to make the column_name metadata for properties forced to upper-case sub table_and_column_names_are_upper_case { 0; } # Basic, dumb data sources do not support joins within a single # query. Instead the Context logic can perform a cross datasource # join within irs own code sub does_support_joins { 0; } # Most datasources do not support recursive queries # Oracle and Postgres do, but in different ways # For data sources without support, it'll have to do multiple queries # to get all the data sub does_support_recursive_queries { ''; } { no warnings 'once'; *create_dbh = \&create_default_handle_wrapper; } sub create_default_handle_wrapper { my $self = UR::Util::object(shift); $self->__signal_observers__('precreate_handle'); my $h = $self->create_default_handle; $self->__signal_observers__('create_handle', $h); # Hack - This is to avoid infinite recursion in the case where the # handle initializers below try to get the hadle by calling $ds->get_default_handle. # The cached/calculated accessor code will look in this hash key and # return the handle instead of recursing back into the handle creation, and # back to here $self->{get_default_handle} = $h; # Backward compatability for older code that still uses _init_created_dbh if ($self->can('_init_created_dbh')) { $self->_init_created_dbh($h); } else { $self->init_created_handle($h); } return $h; } # basic, dumb datasources do not have a handle sub create_default_handle { undef } sub disconnect { } # derived classes can implement this to do extra initialization after the # handle is created sub init_created_handle { 1; } # Peek into the object and see if there's anything in 'get_default_handle' without actually # creating a handle *has_default_dbh = \&has_default_handle; sub has_default_handle { my $self = UR::Util::object(shift); return exists($self->{get_default_handle}); } *disconnect_default_dbh = \&disconnect_default_handle; sub disconnect_default_handle { my $self = shift; if ($self->has_default_handle) { $self->__signal_observers__('predisconnect_handle'); $self->disconnect(); $self->__signal_observers__('disconnect_handle'); } 1; } our $use_dummy_autogenerated_ids; *use_dummy_autogenerated_ids = \$ENV{UR_USE_DUMMY_AUTOGENERATED_IDS}; sub use_dummy_autogenerated_ids { # This allows the saved SQL from sync database to be comparable across executions. # It also my $class = shift; if (@_) { ($use_dummy_autogenerated_ids) = @_; } $use_dummy_autogenerated_ids ||= 0; # Replace undef with 0 return $use_dummy_autogenerated_ids; } our $last_dummy_autogenerated_id; sub next_dummy_autogenerated_id { unless($last_dummy_autogenerated_id) { my $hostname = hostname(); $hostname =~ /(\d+)/; my $id = $1 ? $1 : 1; $last_dummy_autogenerated_id = ($id * -10_000_000) - ($$ * 1_000); } #limit id to fit within 11 characters ($last_dummy_autogenerated_id) = $last_dummy_autogenerated_id =~ m/(-\d{1,10})/; return --$last_dummy_autogenerated_id; } sub autogenerate_new_object_id_for_class_name_and_rule { my $ds = shift; if (ref $ds) { $ds = ref($ds) . " ID " . $ds->id; } # Maybe we could use next_dummy_autogenerated_id instead? die "Data source $ds did not implement autogenerate_new_object_id_for_class_name_and_rule()"; } # UR::Context needs to know if a data source supports savepoints sub can_savepoint { my $class = ref($_[0]); die "Class $class didn't supply can_savepoint()"; } sub set_savepoint { my $class = ref($_[0]); die "Class $class didn't supply set_savepoint, but can_savepoint is true"; } sub rollback_to_savepoint { my $class = ref($_[0]); die "Class $class didn't supply rollback_to_savepoint, but can_savepoint is true"; } sub _get_class_data_for_loading { my ($self, $class_meta) = @_; my $class_data = $class_meta->{loading_data_cache}; unless ($class_data) { $class_data = $self->_generate_class_data_for_loading($class_meta); } return $class_data; } sub _resolve_query_plan { my ($self, $rule_template) = @_; my $qp = UR::DataSource::QueryPlan->get( rule_template => $rule_template, data_source => $self, ); $qp->_init() unless $qp->_is_initialized; return $qp; } # Child classes can override this to return a different datasource # depending on the rule passed in sub resolve_data_sources_for_rule { return $_[0]; } sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $class_name = $class_meta->class_name; my $ghost_class = $class_name->ghost_class; my @all_id_property_names = $class_meta->all_id_property_names(); my @id_properties = $class_meta->id_property_names; my $id_property_sorter = $class_meta->id_property_sorter; my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my @parent_class_objects = $class_meta->ancestry_class_metas; my $sub_classification_method_name; my ($sub_classification_meta_class_name, $subclassify_by); my @all_properties; my $first_table_name; my %seen; for my $co ( $class_meta, @parent_class_objects ) { next if ($seen{ $co->id })++; my $table_name = $co->table_name || '__default__'; $first_table_name ||= $table_name; $sub_classification_method_name ||= $co->sub_classification_method_name; $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_properties, map { [$co, $_, $table_name, 0]} sort $sort_sub UR::Object::Property->get(class_name => $co->class_name); } my $sub_typing_property = $class_meta->subclassify_by; my $class_table_name = $class_meta->table_name; my $class_data = { class_name => $class_name, ghost_class => $class_name->ghost_class, parent_class_objects => [$class_meta->ancestry_class_metas], ## sub_classification_method_name => $sub_classification_method_name, sub_classification_meta_class_name => $sub_classification_meta_class_name, subclassify_by => $subclassify_by, all_properties => \@all_properties, all_id_property_names => [$class_meta->all_id_property_names()], id_properties => [$class_meta->id_property_names], id_property_sorter => $class_meta->id_property_sorter, sub_typing_property => $sub_typing_property, # these seem like they go in the RDBMS subclass, but for now the # "table" concept is stretched to mean any valid structure identifier # within the datasource. first_table_name => $first_table_name, class_table_name => $class_table_name, }; return $class_data; } sub _generate_loading_templates_arrayref { # Each entry represents a table alias in the query. # This accounts for different tables, or multiple occurrances # of the same table in a join, by grouping by alias instead of # table. my $class = shift; my $db_cols = shift; my $obj_joins = shift; my $bxt = shift; use strict; use warnings; my %obj_joins_by_source_alias; if (0) { # ($obj_joins) { my @obj_joins = @$obj_joins; while (@obj_joins) { my $foreign_alias = shift @obj_joins; my $data = shift @obj_joins; for my $foreign_property_name (sort keys %$data) { next if $foreign_property_name eq '-is_required'; my $source_alias = $data->{$foreign_property_name}{'link_alias'}; my $detail = $obj_joins_by_source_alias{$source_alias}{$foreign_alias} ||= {}; # warnings come from the above because we don't have 'link_alias' in filters. my $source_property_name = $data->{$foreign_property_name}{'link_property_name'}; if ($source_property_name) { # join my $links = $detail->{links} ||= []; push @$links, $foreign_property_name, $source_property_name; } if (exists $data->{value}) { # filter my $operator = $data->{operator}; my $value = $data->{value}; my $filter = $detail->{filter} ||= []; my $key = $foreign_property_name; $key .= ' ' . $operator if $operator; push @$filter, $key, $value; } } } } else { #Carp::cluck("no obj joins???"); } my %templates; my $pos = 0; my @templates; my %alias_object_num; for my $col_data (@$db_cols) { my ($class_obj, $prop, $table_alias, $object_num) = @$col_data; unless (defined $object_num) { die "No object num for loading template data?!"; } #Carp::confess() unless $table_alias; my $template = $templates[$object_num]; unless ($template) { $template = { object_num => $object_num, table_alias => $table_alias, data_class_name => $class_obj->class_name, final_class_name => $class_obj->class_name, property_names => [], column_positions => [], id_property_names => undef, id_column_positions => [], id_resolver => undef, # subref }; $templates[$object_num] = $template; $alias_object_num{$table_alias} = $object_num; } push @{ $template->{property_names} }, $prop->property_name; push @{ $template->{column_positions} }, $pos; $pos++; } # remove joins that resulted in no template, such as when it was to a table-less class @templates = grep { $_ } @templates; # Post-process the template objects a bit to get the exact id positions. for my $template (@templates) { my @id_property_names; for my $id_class_name ($template->{data_class_name}, $template->{data_class_name}->inheritance) { my $id_class_obj = UR::Object::Type->get(class_name => $id_class_name); last if @id_property_names = $id_class_obj->id_property_names; } $template->{id_property_names} = \@id_property_names; my @id_column_positions; for my $id_property_name (@id_property_names) { for my $n (0..$#{ $template->{property_names} }) { if ($template->{property_names}[$n] eq $id_property_name) { push @id_column_positions, $template->{column_positions}[$n]; last; } } } $template->{id_column_positions} = \@id_column_positions; if (@id_column_positions == 1) { $template->{id_resolver} = sub { return $_[0][$id_column_positions[0]]; } } elsif (@id_column_positions > 1) { my $class_name = $template->{data_class_name}; $template->{id_resolver} = sub { my $self = shift; return $class_name->__meta__->resolve_composite_id_from_ordered_values(@$self[@id_column_positions]); } } else { Carp::croak("Can't determine which columns will hold the ID property data for class " . $template->{data_class_name} . ". It's ID properties are (" . join(', ', @id_property_names) . ") which do not appear in the class' property list (" . join(', ', @{$template->{'property_names'}}).")"); } my $source_alias = $template->{table_alias}; if (0 and my $join_data_for_source_table = $obj_joins_by_source_alias{$source_alias}) { # there are joins which come from this entity to other entities # as these entities are loaded, remember the individual queries covered by this object returning # NOTE: when we join a <> b, we remember that we've loaded all of the b for a when _a_ loads, not b, # since it's possible that there ar zero of b, and we don't want to perform the query for b my $source_object_num = $template->{object_num}; my $source_class_name = $template->{data_class_name}; my $next_joins = $template->{next_joins} ||= []; for my $foreign_alias (keys %$join_data_for_source_table) { my $foreign_object_num = $alias_object_num{$foreign_alias}; Carp::confess("no alias for $foreign_alias?") if not defined $foreign_object_num; my $foreign_template = $templates[$foreign_object_num]; my $foreign_class_name = $foreign_template->{data_class_name}; my $join_data = $join_data_for_source_table->{$foreign_alias}; my %links = map { $_ ? @$_ : () } $join_data->{links}; my %filters = map { $_ ? @$_ : () } $join_data->{filters}; my @keys = sort (keys %links, keys %filters); my @value_position_source_property; for (my $n = 0; $n < @keys; $n++) { my $key = $keys[$n]; if ($links{$key} and $filters{$key}) { Carp::confess("unexpected same key $key in filters and joins"); } my $source_property_name = $links{$key}; next unless $source_property_name; push @value_position_source_property, $n, $source_property_name; } my $bx = $foreign_class_name->define_boolexpr(map { $_ => $filters{$_} } @keys); my ($bxt, @values) = $bx->template_and_values(); push @$next_joins, [ $bxt->id, \@values, \@value_position_source_property ]; } } } return \@templates; } sub create_iterator_closure_for_rule_template_and_values { my ($self, $rule_template, @values) = @_; my $rule = $rule_template->get_rule_for_values(@values); return $self->create_iterator_closure_for_rule($rule); } sub _reclassify_object_loading_info_for_new_class { my $self = shift; my $loading_info = shift; my $new_class = shift; my $new_info; %$new_info = %$loading_info; foreach my $template_id (keys %$loading_info) { my $target_class_rules = $loading_info->{$template_id}; foreach my $rule_id (keys %$target_class_rules) { my $pos = index($rule_id,'/'); $new_info->{$template_id}->{$new_class . "/" . substr($rule_id,$pos+1)} = 1; } } return $new_info; } sub _get_object_loading_info { my $self = shift; my $obj = shift; my %param_load_hash; if ($obj->{'__load'}) { while( my($template_id, $rules) = each %{ $obj->{'__load'} } ) { foreach my $rule_id ( keys %$rules ) { $param_load_hash{$template_id}->{$rule_id} = $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; } } } return \%param_load_hash; } sub _add_object_loading_info { my $self = shift; my $obj = shift; my $param_load_hash = shift; while( my($template_id, $rules) = each %$param_load_hash) { foreach my $rule_id ( keys %$rules ) { $obj->{'__load'}->{$template_id}->{$rule_id} = $rules->{$rule_id}; } } } # same as add_object_loading_info, but manipulates the data in $UR::Context::all_params_loaded sub _record_that_loading_has_occurred { my $self = shift; my $param_load_hash = shift; while( my($template_id, $rules) = each %$param_load_hash) { foreach my $rule_id ( keys %$rules ) { $UR::Context::all_params_loaded->{$template_id}->{$rule_id} ||= $rules->{$rule_id}; } } } sub _first_class_in_inheritance_with_a_table { # This is called once per subclass and cached in the subclass from then on. my $self = shift; my $class = shift; $class = ref($class) if ref($class); unless ($class) { Carp::confess("No class?"); } my $class_object = $class->__meta__; my $found = ""; for ($class_object, $class_object->ancestry_class_metas) { if ($_->table_name) { $found = $_->class_name; last; } } #eval qq/ # package $class; # sub _first_class_in_inheritance_with_a_table { # return '$found' if \$_[0] eq '$class'; # shift->SUPER::_first_class_in_inheritance_with_a_table(\@_); # } #/; #die "Error setting data in subclass: $@" if $@; return $found; } sub _class_is_safe_to_rebless_from_parent_class { my ($self, $class, $was_loaded_as_this_parent_class) = @_; my $fcwt = $self->_first_class_in_inheritance_with_a_table($class); unless ($fcwt) { Carp::croak("Can't call _class_is_safe_to_rebless_from_parent_class(): Class $class has no parent classes with a table"); } return ($was_loaded_as_this_parent_class->isa($fcwt)); } sub ur_datasource_class_for_dbi_connect_string { my($class, $dsn) = @_; my(undef, $driver) = DBI->parse_dsn($dsn); $driver || Carp::croak("Could not parse DBI driver out of connect string $dsn"); return 'UR::DataSource::'.$driver; } sub _get_current_entities { my $self = shift; my @class_meta = UR::Object::Type->is_loaded( data_source_id => $self->id ); my @objects; for my $class_meta (@class_meta) { next unless $class_meta->generated(); # Ungenerated classes won't have any instances my $class_name = $class_meta->class_name; push @objects, $UR::Context::current->all_objects_loaded($class_name); } return @objects; } sub _prepare_for_lob { }; sub _set_specified_objects_saved_uncommitted { my ($self,$objects_arrayref) = @_; # Sets an objects as though the has been saved but tha changes have not been committed. # This is called automatically by _sync_databases. my %objects_by_class; my $class_name; for my $object (@$objects_arrayref) { $class_name = ref($object); $objects_by_class{$class_name} ||= []; push @{ $objects_by_class{$class_name} }, $object; } for my $class_name (sort keys %objects_by_class) { my $class_object = $class_name->__meta__; my @property_names = map { $_->property_name } grep { $_->column_name } $class_object->all_property_metas; for my $object (@{ $objects_by_class{$class_name} }) { $object->{db_saved_uncommitted} ||= {}; my $db_saved_uncommitted = $object->{db_saved_uncommitted}; for my $property ( @property_names ) { $db_saved_uncommitted->{$property} = $object->$property; } } } return 1; } sub _set_all_objects_saved_committed { # called by UR::DBI on commit my $self = shift; return $self->_set_specified_objects_saved_committed([ $self->_get_current_entities ]); } sub _set_all_specified_objects_saved_committed { my $self = shift; my($pkg, $file, $line) = caller; Carp::carp("Deprecated method _set_all_specified_objects_saved_committed called at file $file line $line. The new name for this method is _set_specified_objects_saved_committed"); my @changed_objects = @_; $self->_set_specified_objects_saved_committed(\@changed_objects); } sub _set_specified_objects_saved_committed { my $self = shift; my $objects = shift; # Two step process... set saved and committed, then fire commit observers. # Doing so prevents problems should any of the observers themselves commit. my @saved_objects; for my $obj (@$objects) { my $saved = $self->_set_object_saved_committed($obj); push @saved_objects, $saved if $saved; } for my $obj (@saved_objects) { next if $obj->isa('UR::DeletedRef'); $obj->__signal_change__('commit'); if ($obj->isa('UR::Object::Ghost')) { $UR::Context::current->_abandon_object($obj); } } return scalar(@$objects) || "0 but true"; } sub _set_object_saved_committed { # called by the above, and some test cases my ($self, $object) = @_; if ($object->{db_saved_uncommitted}) { unless ($object->isa('UR::Object::Ghost')) { %{ $object->{db_committed} } = ( ($object->{db_committed} ? %{ $object->{db_committed} } : ()), %{ $object->{db_saved_uncommitted} } ); delete $object->{db_saved_uncommitted}; } return $object; } else { return; } } sub _set_all_objects_saved_rolled_back { # called by UR::DBI on commit my $self = shift; my @objects = $self->_get_current_entities; for my $obj (@objects) { unless ($self->_set_object_saved_rolled_back($obj)) { die "An error occurred setting " . $obj->__display_name__ . " to match the rolled-back database state. Exiting..."; } } } sub _set_specified_objects_saved_rolled_back { my $self = shift; my $objects = shift; for my $obj (@$objects) { unless ($self->_set_object_saved_rolled_back($obj)) { die "An error occurred setting " . $obj->__display_name__ . " to match the rolled-back database state. Exiting..."; } } } sub _set_object_saved_rolled_back { # called by the above, and some test cases my ($self,$object) = @_; delete $object->{db_saved_uncommitted}; return $object; } # These are part of the basic DataSource API. Subclasses will want to override these sub _sync_database { my $class = shift; my %args = @_; $class = ref($class) || $class; $class->warning_message("Data source $class does not support saving objects to storage. " . scalar(@{$args{'changed_objects'}}) . " objects will not be saved"); return 1; } sub commit { my $class = shift; my %args = @_; $class = ref($class) || $class; #$class->warning_message("commit() ignored for data source $class"); return 1; } sub rollback { my $class = shift; my %args = @_; $class = ref($class) || $class; $class->warning_message("rollback() ignored for data source $class"); return 1; } # When the class initializer is create property objects, it will # auto-fill-in column_name if the class definition has a table_name. # File-based data sources do not have tables (and so classes using them # do not have table_names), but the properties still need column_names # so loading works properly. # For now, only UR::DataSource::File and ::FileMux set this. # FIXME this method's existence is ugly. Find a better way to fill in # column_name for those properties, or fix the data sources to not # require column_names to be set by the initializer sub initializer_should_create_column_name_for_class_properties { return 0; } # Subclasses should override this. # It's called by the class initializer when the data_source property in a class # definition contains a hashref with an 'is' key. The method should accept this # hashref, create a data_source instance (if appropriate) and return the class_name # of this new datasource. sub create_from_inline_class_data { my ($class,$class_data,$ds_data) = @_; my %ds_data = %$ds_data; my $ds_class_name = delete $ds_data{is}; unless (my $ds_class_meta = UR::Object::Type->get($ds_class_name)) { die "No class $ds_class_name found!"; } my $ds = $ds_class_name->__define__(%ds_data); unless ($ds) { die "Failed to construct $ds_class_name: " . $ds_class_name->error_message(); } return $ds; } sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; return [undef,undef]; # The default that should give reasonable behavior } # prepare_for_fork, do_after_fork_in_child, and finish_up_after_fork are no-op # here in the UR::DataSource base class and should be implented in subclasses # as needed. sub prepare_for_fork { return 1 } sub do_after_fork_in_child { return 1 } sub finish_up_after_fork { return 1 } sub _resolve_owner_and_table_from_table_name { my($self, $table_name) = @_; # Basic data sources don't know about owners/schemas return (undef, $table_name); } sub _resolve_table_and_column_from_column_name { my($self, $column_name) = @_; # Basic data sources don't know about tables return (undef,$column_name); } 1; DataSource.pod100664023532023421 2242612544604516 15515 0ustar00abrummetgsc000000000000UR-0.44/lib/UR=pod =head1 NAME UR::DataSource - manage the the relationship between objects and a specific storage system =head1 SYNOPSIS package MyApp::DataSource::DB; class MyApp::DataSource::DB { is => ['UR::DataSource::Oracle','UR::Singleton'], }; sub server { 'main_db_server' } sub login { 'db_user' } sub auth { 'db_passwd' } sub owner { 'db_owner' } 1; =head1 DESCRIPTION Data source instances represent a logical souce of data to the application. Most of them are likely to be some kind of relational database, but not all are. UR::DataSource is an abstract base class inherited by other data sources. In normal use, your data sources will probably inherit from an abstract data source class such as L or L, as well as L. This makes it easy to link classes to this data source, since the class name will be the same as its ID, and the module autoloader will instantiate it automatically. =head1 INHERITANCE L =head1 Methods User applications will seldom interact with data sources directly. =over 4 =item autogenerate_new_object_id_for_class_name_and_rule my $id = $datasource->autogenerate_new_object_id_for_class_name_and_rule($class,$boolexpr); L calls this when the application calls create() on a class to create a new instance, but does not specify a value for the ID property. The default implementation throws an exception with C, but L is able to query a sequence in the database to generate unique IDs. A developer implementing a new data source will need to override this method and provide a sensible implementation. =item next_dummy_autogenerated_id my $int = $datasource->next_dummy_autogenerated_id() In a testing situation, is often preferable to avoid using the database's sequence for ID autogeneration but still make ID values that are unique. L calls this method if the L (see below) flag is true. The IDs generated by this method are unique during the life of the process. In addition, objects with dummy-generated IDs will never be saved to a real data source during UR::Context::commit(). =item use_dummy_autogenerated_ids $bool = $datasource->use_dummy_autogenerated_ids(); $datasource->use_dummy_autogenerated_ids($bool); Get or set a flag controlling how object IDs are autogenerated. Data source child classes should look at the value of this flag inside their implementation of C. If true, they should call C and return that value instead of attempting to generate an ID on their own. This flag is also tied to the UR_USE_DUMMY_AUTOGENERATED_IDS environment variable. =item resolve_data_sources_for_rule $possibly_other_data_source = $data_source->resolve_data_sources_for_rule($boolexpr); When L is determining which data source to use to process a get() request, it looks at the class metadata for its data source, and then calls C to give that data source a chance to defer to another data source. =item create_iterator_closure_for_rule_template_and_values $subref = $datasource->create_iterator_closure_for_rule_template_and_values( $boolexpr_tmpl, @values ); A front-end for the more widely used L =item create_iterator_closure_for_rule $subref = $datasource->create_iterator_closure_for_rule($boolexpr); This is the main entry point L uses to get data from its underlying data sources. There is no default implementation; each subclass implementing specific data source types must supply its own code. The method must accept a L $boolexpr (rule), and return a subref. Each time the subref is called it must return one arrayref of data satisfying the rule, and undef when there is no more data to return. =item _sync_database $bool = $datasource->_sync_database(changed_objects => $listref); Called by L commit(). $listref will contain all the changed objects that should be saved to that data source. The default implementation prints a warning message and returns true without saving anything. L has a functioning _sync_database() capable of generating SQL to update, insert and delete rows from the database's tables. The data source should return true if all the changes were successfully made, false if there were problems. =item commit $bool = $datasource->commit() Called by L commit(). After all data sources return true from _sync_database(), C must make those changes permanent. For RDBMS-type data sources, this commits the transaction. Return true if the commit is successful, false otherwise. =item rollback $bool = $datasource->rollback() Called by L if any data sources has problems during _sync_database or commit. It is also called by L. Data sources should reverse any changes applied during a prior C<_sync_database> that has not been made permanent by C. =item get_default_handle $scalar = $datasource->get_default_handle(); Should return the "handle" associated with any underlying logical data. For an RDBMS data source, this is the L database handle. For a file-based data source, this is the file handle. =item create_from_inline_class_data $datasource = $data_source_class_name->create_from_inline_class_data( $class_data_hashref, $datasource_data_hashref ); Called by the class initializer when a class definition contains an in-line data source definition. See L. =item _ignore_table $bool = $datasource->_ignore_table($table_name); Used to indicate whether the C command should create a class for the named table or not. If _ignore_table() returns true, then it will not create a class. =back =head1 Internal API Methods =over 4 =item _get_class_data_for_loading =item _generate_class_data_for_loading $hashref = $datasource->_resolve_query_plan($class_meta); These two methods are called by L as part of the object loading process. C<_generate_class_data_for_loading> collects information about a class and its metadata, such as property names, subclassing information and tables connected to the class, and stores that data inside the class's metadata object. C<_get_class_data_for_loading> is the main entry point; it calls C<_generate_class_data_for_loading> if the data has not been generated and cached yet, and caches the data in the class metadata object. =item _resolve_query_plan =item _generate_template_data_for_loading $hashref = $datasource->_resolve_query_plan($boolexpr_tmpl); These two methods are called by L as part of the object loading process. C<_generate_template_data_for_loading> collects information from the L $boolexpr_tmpl (rule template) and returns a hashref used later by the data source. This hashref includes hints about what classes will be involved in loading the resulting data, how those classes are joined together and how columns in the underlying query against the data source will map to properties of the class. C<_resolve_query_plan> is the main entry point; it calls C<_generate_template_data_for_loading> if the data has not been generated and cached yet, and caches the data in the rule template object. =item _generate_loading_templates_arrayref my $listref = $datasource->_generate_loading_templates_arrayref($listref); Called by _generate_template_data_for_loading(). The input is a listref of listrefs about properties involved in a query. The second-level data is sets of quads: =over =item 1. The class object for this property =item 2. The property metadata object =item 3. The database table name the data will come from =item 4 The "object number", starting with 0. This is used in inheritance or delegation where a table join will be required. =back It returns a listref of hashrefs, one hashref for every class involved in the request; usually just 1, but can be more than one if inheritance or delegation is involved. The data includes information about the class's properties, ID properties, and which columns of the result set the values will be found. =back =head1 MetaDB Each Namespace created through C will have a data source called the MetaDB. For example, the MyApp namespace's MetaDB is called MyApp::DataSource::Meta. The MetaDB is used to store information about the schemas of other data sources in the database. UR itself has a MetaDB with information about the MetaDB's schema, called L. =head1 SEE ALSO =over 4 =item L The base class for relational database Data Sources, such as L, L, L and L =item L, The base class for comma/tab delimited files =item L The base class for file multiplexor data sources. =back L, L CSV.pm100664023532023421 2321112544604516 15773 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::CSV; use strict; use warnings; # There are still a few issues with actually using this thing: # # when running # ur define datasource rdbms --dsn DBD:CSV:f_dir=/tmp/trycsv # the f_dir=... part doesn't get put in as a "server" attribute. # You have to add in it by hand as sub server {} # # after ur update classes, there aren't any id properties defined # for your new classes, because there's no conclusive way to pick # the right one - no unique constraints # # _get_sequence_name_for_table_and_column() and _get_next_value_from_sequence() # aren't implemented yet, so creating new entities and sync_databases # won't work # # There's a bug in even the latest SQL::Statement on CPAN where the processing # of JOIN clauses uses a case sensitive match against upper-case stuff, when it # should be lower-case. It also cannot handle more than one join in the same # statement # # with that out of the way... on to the show! require UR; our $VERSION = "0.44"; # UR $VERSION; use File::Basename; UR::Object::Type->define( class_name => 'UR::DataSource::CSV', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "CSV" } sub owner { undef } sub login { undef } sub auth { undef } sub path { my $server = shift->server; my @server_opts = split(';', $server); foreach my $opt ( @server_opts ) { my($key,$value) = split('=',$opt); if ($key eq 'f_dir') { return $value; } } return; } sub can_savepoint { 0;} # Dosen't support savepoints sub _dbi_connect_args { my $self = shift; my @connection = $self->SUPER::_dbi_connect_args(@_); delete $connection[3]->{'AutoCommit'}; # DBD::CSV doesn't support autocommit being off return @connection; } sub _get_sequence_name_for_table_and_column { Carp::croak("Not implemented yet"); my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_seq', $table_name, $column_name); unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTOINCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { Carp::croak('Not implemented yet'); my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } # Given a table name, return the complete pathname to it # As ur update classes calls this, $table has been uppercased # already (because most data sources uppercase table names), # so we need to figure out which file they're talking about sub _find_pathname_for_table { my $self = shift; my $table = shift; my $path = $self->path; my @all_files = glob("$path/*"); # note: this only finds the first one foreach my $pathname ( @all_files ) { if (File::Basename::basename($pathname) eq $table) { return $pathname; } } return; } # column_info doesn't work against a DBD::CSV handle sub get_column_details_from_data_dictionary { my($self,$catalog,$schema,$table,$column) = @_; # Convert the SQL wildcards to glob wildcards $table =~ tr/%_/*?/; # Convert the SQL wildcards to regex wildcards $column =~ s/%/\\w*/; $column =~ s/_/\\w/; my $column_regex = qr($column); my(@matching_files) = $self->_find_pathname_for_table($table); my @found_columns; foreach my $file ( @matching_files ) { my $table_name = File::Basename::basename($file); my $fh = IO::File->new($file); unless ($fh) { $self->warning_message("Can't open file $file for reading: $!"); next; } my $header = $fh->getline(); $header =~ s/\r|\n//g; # Remove newline/CR my @columns = split($self->get_default_handle->{'csv_sep_char'} ||',' , $header); my $column_order = 0; foreach my $column_name ( @columns ) { $column_order++; next unless $column_name =~ m/$column_regex/; push @found_columns, { TABLE_CAT => $catalog, TABLE_SCHEM => $schema, TABLE_NAME => $table_name, COLUMN_NAME => $column_name, DATA_TYPE => 'STRING', # what else could we put here? TYPE_NAME => 'STRING', NULLABLE => 1, # all columns are nullable in CSV files IS_NULLABLE => 'YES', REMARKS => '', COLUMN_DEF => '', SQL_DATA_TYPE => '', # FIXME shouldn't this be something related to DATA_TYPE SQL_DATETIME_SUB => '', CHAH_OCTET_LENGTH => undef, # FIXME this should be the same as column_size, right? ORDINAL_POSITION => $column_order, } } } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE ); my $returned_sth = $sponge->prepare("column_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @found_columns ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # DBD::CSV doesn't support foreign key tracking # returns a statement handle with no data to read sub get_foreign_key_details_from_data_dictionary { my($self,$fk_catalog,$fk_schema,$fk_table,$pk_catalog,$pk_schema,$pk_table) = @_; my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( FK_NAME UK_TABLE_NAME UK_COLUMN_NAME FK_TABLE_NAME FK_COLUMN_NAME ); my $table = $pk_table || $fk_table; my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } sub get_bitmap_index_details_from_data_dictionary { # DBD::CSV dosen't support bitmap indicies, so there aren't any return []; } sub get_unique_index_details_from_data_dictionary { # DBD::CSV doesn't support unique constraints return {}; } sub get_table_details_from_data_dictionary { my($self,$catalog,$schema,$table,$type) = @_; # DBD::CSV's table_info seems to always give you back all the "tables" even # if you only asked for details on one of them my $sth = $self->SUPER::get_table_details_from_data_dictionary($catalog,$schema,$table,$type); # Yeah, it's kind of silly to have to read in all the data and repackage it # back into another sth my @returned_details; while (my $row = $sth->fetchrow_arrayref()) { next unless ($row->[2] eq $table); push @returned_details, $row; } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS ); my $returned_sth = $sponge->prepare("table_info $table", { rows => \@returned_details, NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->DBI::set_err($sponge->err(), $sponge->errstr()); $returned_sth; } # By default, make a text dump of the database at commit time. # This should really be a datasource property sub dump_on_commit { 0; } 1; =pod =head1 NAME UR::DataSource::CSV - Parent class for data sources using DBD::CSV =head1 DESCRIPTION UR::DataSource::CSV is a subclass of L and can be used for interacting with CSV files. Because of the limitations of the underlying modules (such as SQL::Statement only supporting one join at a time), this module is deprecated. L implements a non-SQL interface for data files, and is the proper way to use a file as a data source for class data. =cut Code.db100664023532023421 1200012544604516 16155 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourceSQLite format 3@  7ï%Å7 K/)indexmethod_name_usage_i_method_namemethod_name_usageCREATE INDEX method_name_usage_i_method_name on method_name_usage(method_name)^5}indexmethod_i_method_namemethodCREATE INDEX method_i_method_name on method(method_name)G//‚=tablemethod_name_usagemethod_name_usageCREATE TABLE method_name_usage ( file_name varchar2(255), method_name varchar2(255), line_number integer )‚ƒwtablemethodmethodCREATE TABLE method ( file_name varchar2(255), class_name varchar2(255), method_name varchar2(255), line_number integer, line_count integer, is_deprecated bool )    Code.pm100664023532023421 265712544604516 16205 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource package UR::DataSource::Code; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use File::Copy qw//; ##- use UR; UR::Object::Type->define( class_name => 'UR::DataSource::Code', is => ['UR::DataSource::SQLite'], ); sub server { my $self = shift->_singleton_object(); my $path = $self->__meta__->module_path; $path =~ s/\.pm$/.db/ or Carp::confess("Bad module path for resolving server!"); unless (-e $path) { # initialize a new database from the one in the base class # should this be moved to connect time? my $template_database_file = UR::DataSource::Code->server(); if ($self->class eq __PACKAGE__) { Carp::confess("Missing template database file: $path!"); } unless (-e $template_database_file) { Carp::confess("Missing template database file: $path! Cannot initialize database for " . $self->class); } unless(File::Copy::copy($template_database_file,$path)) { Carp::confess("Error copying $path to $template_database_file to initialize database!"); } unless(-e $path) { Carp::confess("File $path not found after copy from $template_database_file. Cannot initialize database!"); } } return $path; } sub resolve_class_name_for_table_name_fixups { my $self = shift->_singleton_object; print "fixup @_"; return $self->class . "::", @_; } 1; Code.schema100664023532023421 103312544604516 17014 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourceCREATE TABLE method ( file_name varchar2(255), class_name varchar2(255), method_name varchar2(255), line_number integer, line_count integer, is_deprecated bool ); CREATE TABLE method_name_usage ( file_name varchar2(255), method_name varchar2(255), line_number integer ); CREATE INDEX method_i_method_name on method(method_name); CREATE INDEX method_name_usage_i_method_name on method_name_usage(method_name); Default.pm100664023532023421 1071312544604516 16727 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::Default; # NOTE: UR::DataSource::QueryPlan currently has conditional logic for this class use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; class UR::DataSource::Default { is => ['UR::DataSource','UR::Singleton'], doc => 'allows the class to describe its own loading strategy' }; sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $subject_class_name = $rule->subject_class_name; unless ($subject_class_name->can('__load__')) { Carp::croak("Can't load from class $subject_class_name: UR::DataSource::Default requires the class to implement __load__"); } my $template = $rule->template; my ($query_plan) = $self->_resolve_query_plan($template); my $expected_headers = $query_plan->{loading_templates}[0]{property_names}; my ($headers, $content) = $subject_class_name->__load__($rule,$expected_headers); my $iterator; if (ref($content) eq 'ARRAY') { $iterator = sub { my $next_row = shift @$content; $content = undef if @$content == 0; return $next_row; }; } elsif (ref($content) eq 'CODE') { $iterator = $content; } else { Carp::confess("Expected an arrayref of properties, and then content in the form of an arrayref (rows,columns) or coderef/iterator returning rows from $subject_class_name __load__!\n"); } if ("@$headers" ne "@$expected_headers") { # translate the headers into the appropriate order my @mapping = eval { _map_fields($headers,$expected_headers);}; if ($@) { Carp::croak("Loading data for class $subject_class_name and boolexpr $rule failed: $@"); } # print Data::Dumper::Dumper($headers,$expected_headers,\@mapping); my $orig_iterator = $iterator; $iterator = sub { my $result = $orig_iterator->(); return unless $result; my @result2 = @$result[@mapping]; return \@result2; }; } return $iterator; } sub can_savepoint { 0 } sub _map_fields { my ($from,$to) = @_; my $n = 0; my %from = map { $_ => $n++ } @$from; my @pos; for my $field (@$to) { my $pos = $from{$field}; unless (defined $pos) { #print "@$from\n@$to\n" . Carp::longmess() . "\n"; die("Can't resolve value for '$field' from the headers returned by its __load__: ". join(', ', @$from)); } push @pos, $pos; } return @pos; } # Nothing to be done for rollback sub rollback { 1;} my @saved_objects; sub _sync_database { my $self = shift; my %params = @_; my $changed_objects = $params{changed_objects}; my %class_can_save; my $err = do { local $@; eval { for my $obj (@$changed_objects) { my $obj_class = $obj->class; unless (exists $class_can_save{$obj_class}) { $class_can_save{$obj_class} = $obj->can('__save__'); } if ($class_can_save{$obj_class}) { push @saved_objects, $obj; $obj->__save__; } } }; $@; }; if ($err) { my @failed_rollback; do { my $rollback_error; while (my $obj = shift @saved_objects) { local $@; eval { $obj->__rollback__; }; if ($@) { $rollback_error = $@; push @failed_rollback, $obj; } } if (@failed_rollback) { $self->error_message('Rollback failed: ' . Data::Dumper::Dumper(\@failed_rollback)); Carp::croak "Failed to save, and ERRORS DURING ROLLBACK:\n$err\n $rollback_error\n"; } }; die $err; } return 1; } sub commit { my @failed_commit; while (my $obj = shift @saved_objects) { local $@; eval { $obj->__commit__; }; if ($@) { push @failed_commit, $@ => $obj; } } if (@failed_commit) { my @failure_messages; for (my $i = 0; $i < @failed_commit; $i += 2) { my($exception, $obj) = @failed_commit[$i .. $i+1]; push @failure_messages, "$exception: ".Data::Dumper::Dumper($obj); } Carp::croak "Commit failed:\n" . join("\n", @failure_messages); } return 1; } 1; File.pm100664023532023421 14565012544604516 16253 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::File; # NOTE! This module is deprecated. Use UR::DataSource::Filesystem instead. # A data source implementation for text files where the fields # are delimited by commas (or anything else really). Usually, # the lines in the file will be sorted by one or more columns, # but it isn't strictly necessary # # For now, it's structured around files where the record is delimited by # newlines, and the fields are delimited by qr(\s*,\s*). Those are # overridable in concrete data sources by specifying record_seperator() and # delimiter(). # FIXME - work out a way to support record-oriented data as well as line-oriented data use UR; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; use Fcntl qw(:DEFAULT :flock); use Errno qw(EINTR EAGAIN EOPNOTSUPP); use File::Temp; use File::Basename; use IO::File qw(); our @CARP_NOT = qw( UR::Context UR::DataSource::FileMux UR::Object::Type ); class UR::DataSource::File { is => ['UR::DataSource'], has => [ delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, column_order => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, skip_first_line => { is => 'Integer', default_value => 0, doc => 'Number of lines at the start of the file to skip' }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, quick_disconnect => { is => 'Boolean', default_value => 1, doc => 'Do not hold the file handle open between requests' }, ], has_optional => [ server => { is => 'String', doc => 'pathname to the data file' }, file_list => { is => 'ARRAY', doc => 'list of pathnames of equivalent files' }, sort_order => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, constant_values => { is => 'ARRAY', doc => 'Property names which are not in the data file(s), but are part of the objects loaded from the data source' }, # REMOVE #file_cache_index => { is => 'Integer', doc => 'index into the file cache where the next read will be placed' }, _open_query_count => { is => 'Integer', doc => 'number of queries currently using this data source, used internally' }, ], doc => 'A data source for line-oriented files', }; sub can_savepoint { 0;} # Doesn't support savepoints sub create_default_handle { my $self = shift; if ($ENV{'UR_DBI_MONITOR_SQL'}) { my $time = time(); UR::DBI->sql_fh->printf("\nFILE OPEN AT %d [%s]\n",$time, scalar(localtime($time))); } my $filename = $self->server; unless (-e $filename) { # file doesn't exist $filename = '/dev/null'; } my $handle_class = $self->handle_class; my $fh = $handle_class->new($filename); unless($fh) { $self->error_message("Can't open ".$self->server." for reading: $!"); return; } if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->printf("FILE: opened %s fileno %d\n\n",$self->server, $fh->fileno); } $self->is_connected(1); return $fh; } sub disconnect { my $self = shift; if ($self->has_default_handle) { my $fh = $self->get_default_handle; flock($fh,LOCK_UN); $fh->close(); $self->__invalidate_get_default_handle__; $self->is_connected(0); } } sub _file_position { my $self = shift; my $fh = $self->get_default_handle; return $fh ? $fh->tell() : undef; } sub prepare_for_fork { my $self = shift; # make sure this is clear before we fork $self->{'_fh_position'} = undef; if ($self->has_default_handle) { $self->{'_fh_position'} = $self->_file_position(); UR::DBI->sql_fh->printf("FILE: preparing to fork; closing file %s and noting position at %s\n",$self->server, $self->{'_fh_position'}) if $ENV{'UR_DBI_MONITOR_SQL'}; } $self->disconnect_default_handle; } sub finish_up_after_fork { my $self = shift; if (defined $self->{'_fh_position'}) { UR::DBI->sql_fh->printf("FILE: resetting after fork; reopening file %s and fast-forwarding to %s\n",$self->server, $self->{'_fh_position'}) if $ENV{'UR_DBI_MONITOR_SQL'}; my $fh = $self->get_default_handle; $fh->seek($self->{'_fh_position'},0); } } sub _regex { my $self = shift; unless ($self->{'_regex'}) { my $delimiter = $self->delimiter; my $r = eval { qr($delimiter) }; if ($@ || !$r) { $self->error_message("Unable to interepret delimiter '".$self->delimiter.": $@"); return; } $self->{'_regex'} = $r; } return $self->{'_regex'}; } # We're overriding server() so everyone else can have a single way of getting # the file's pathname instead of having to know about both server and file_list sub server { my $self = shift; unless ($self->{'_cached_server'}) { if ($self->__server()) { $self->{'_cached_server'} = $self->__server(); } elsif ($self->file_list) { my $files = $self->file_list; my $count = scalar(@$files); my $idx = $$ % $count; $self->{'_cached_server'} = $files->[$idx]; } else { die "Data source ",$self->id," didn't specify either server or file_list"; } } return $self->{'_cached_server'}; } # Should be divisible by 3 our $MAX_CACHE_SIZE = 99; # The offset cache is an arrayref containing three pieces of data: # 0: If this cache slot is being used by a loading iterator # 1: concatenated data from the sorted columns for comparison with where you are in the file # 2: the seek position that line came from sub _offset_cache { my $self = shift; unless ($self->{'_offset_cache'}) { $self->{'_offset_cache'} = []; } return $self->{'_offset_cache'}; } our %iterator_data_source; our %iterator_cache_slot_refs; sub _allocate_offset_cache_slot { my $self = shift; my $cache = $self->_offset_cache(); my $next = scalar(@$cache); #print STDERR "_allocate_offset_cache_slot ".$self->server." current size is $next "; if ($next > $MAX_CACHE_SIZE) { #print STDERR "searching... \n"; my $last_offset_cache_slot = $self->{'_last_offset_cache_slot'}; if ($last_offset_cache_slot >= $MAX_CACHE_SIZE) { $next = 0; } else { $next = $last_offset_cache_slot + 3; } # Search for an unused slot while ($cache->[$next] and $next != $last_offset_cache_slot) { $next += 3; $next = 0 if ($next > $MAX_CACHE_SIZE); } if ($next > $MAX_CACHE_SIZE or $next eq $last_offset_cache_slot) { #print STDERR scalar(keys(%iterator_data_source))." items in iterator_data_source ".scalar(keys(%iterator_cache_slot))." in iterator_cache_slot\n"; Carp::carp("Unable to find an open file offset cache slot because there are too many outstanding loading iterators. Temporarily expanding the cache..."); # We'll let it go ahead and expand the list $next = $MAX_CACHE_SIZE; $MAX_CACHE_SIZE += 3; } } $cache->[$next] = 1; $cache->[$next+1] = undef; $cache->[$next+2] = undef; $self->{'_last_offset_cache_slot'} = $next; #print STDERR "using slot $next current size ".scalar(@$cache)."\n"; return $next; } sub _free_offset_cache_slot { my($self, $cache_slot) = @_; my $cache = $self->_offset_cache(); unless ($cache_slot < scalar(@$cache)) { $self->warning_message("Freeing offset cache slot past the end. Current size ".scalar(@$cache).", requested $cache_slot"); return; } unless (defined $cache->[$cache_slot]) { $self->warning_message("Freeing unused offset cache slot $cache_slot"); return; } if ($cache->[$cache_slot+1] and scalar(@{$cache->[$cache_slot+1]}) == 0) { # There's no data in here. Must have happened when the reader went all the # way to the end of the file and found nothing. Remove this entry completely # because it's not helpful splice(@$cache, $cache_slot,3); } else { # There is data in here, mark it as a free slot $cache->[$cache_slot] = 0; } return 1; } sub _invalidate_cache { my $self = shift; $self->{'_offset_cache'} = []; return 1; } sub _generate_loading_templates_arrayref { my($self,$old_sql_cols) = @_; my $columns_in_file = $self->column_order; my %column_to_position_map; for (my $i = 0; $i < @$columns_in_file; $i++) { $column_to_position_map{$columns_in_file->[$i]} = $i; } # strip out columns that don't exist in the file my $sql_cols; foreach my $column_data ( @$old_sql_cols ) { my $propertys_column_name = $column_data->[1]->column_name; next unless ($propertys_column_name and exists($column_to_position_map{$propertys_column_name})); push @$sql_cols, $column_data; } unless ($sql_cols) { $self->error_message("Couldn't determine column information for data source " . $self->id); return; } # reorder the requested columns to be in the same order as the file my @sql_cols_with_column_name = map{ [ $column_to_position_map{ $_->[1]->column_name }, $_ ] } @$sql_cols; my @sorted_sql_cols = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @sql_cols_with_column_name; $sql_cols = \@sorted_sql_cols; my $templates = $self->SUPER::_generate_loading_templates_arrayref($sql_cols); if (my $constant_values = $self->constant_values) { # Find the first unused index in the loading template my $next_template_slot = -1; foreach my $tmpl ( @$templates ) { foreach my $col ( @{$tmpl->{'column_positions'}} ) { if ($col >= $next_template_slot) { $next_template_slot = $col + 1; } } } if ($next_template_slot == -1) { die "Couldn't determine last column in loading template for data source" . $self->id; } foreach my $prop ( @$constant_values ) { push @{$templates->[0]->{'column_positions'}}, $next_template_slot++; push @{$templates->[0]->{'property_names'}}, $prop; } } return $templates; } sub _things_in_list_are_numeric { my $self = shift; foreach ( @{$_[0]} ) { return 0 if (! Scalar::Util::looks_like_number($_)); } return 1; } # Construct a closure to perform a test on the $index-th column of # @$$next_candidate_row. The closures return 0 is the test is successful, # -1 if unsuccessful but the file's value was less than $value, and 1 # if unsuccessful and greater. The iterator that churns throug the file # knows that if it's comparing an ID/sorted column, and the comparator # returns 1 then we've gone past the point where we can expect to ever # find another successful match and we should stop looking my $ALWAYS_FALSE = sub { -1 }; sub _comparator_for_operator_and_property { my($self,$property,$next_candidate_row, $index, $operator,$value) = @_; no warnings 'uninitialized'; # we're handling ''/undef/null specially below where it matters if ($operator eq 'between') { if ($value->[0] eq '' or $value->[1] eq '') { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { if ($value->[0] > $value->[1]) { # Will never be true Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # numeric 'between' comparison return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] < $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] > $value->[1]) { return 1; } else { return 0; } }; } else { if ($value->[0] gt $value->[1]) { Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # A string 'between' comparison return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] lt $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] gt $value->[1]) { return 1; } else { return 0; } }; } } elsif ($operator eq 'in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { # Numeric 'in' comparison returns undef if we're within the range of the list # but don't actually match any of the items in the list @$value = sort { $a <=> $b } @$value; # sort the values first return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] < $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] > $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if $$next_candidate_row->[$index] == $_; } return -1; } }; } else { # A string 'in' comparison @$value = sort { $a cmp $b } @$value; return sub { if ($$next_candidate_row->[$index] lt $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] gt $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if $$next_candidate_row->[$index] eq $_; } return -1; } }; } } elsif ($operator eq 'not in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); foreach ( @$value ) { return -1 if $$next_candidate_row->[$index] == $_; } return 0; } } else { return sub { foreach ( @$value ) { return -1 if $$next_candidate_row->[$index] eq $_; } return 0; } } } elsif ($operator eq 'like') { # 'like' is always a string comparison. In addition, we can't know if we're ahead # or behind in the file's ID columns, so the only two return values are 0 and 1 return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false # Convert SQL-type wildcards to Perl-type wildcards # Convert a % to a *, and _ to ., unless they're preceeded by \ to escape them. # Not that this isn't precisely correct, as \\% should really mean a literal \ # followed by a wildcard, but we can't be correct in all cases without including # a real parser. This will catch most cases. $value =~ s/(?[$index] eq ''); if ($$next_candidate_row->[$index] =~ $regex) { return 0; } else { return 1; } }; } elsif ($operator eq 'not like') { return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false $value =~ s/(?[$index] eq ''); if ($$next_candidate_row->[$index] =~ $regex) { return 1; } else { return 0; } }; # FIXME - should we only be testing the numericness of the property? } elsif ($property->is_numeric and $self->_things_in_list_are_numeric([$value])) { # Basic numeric comparisons if ($operator eq '=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number return $$next_candidate_row->[$index] <=> $value; }; } elsif ($operator eq '<') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] < $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] <= $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] > $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] >= $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { $$next_candidate_row->[$index] ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { $$next_candidate_row->[$index] ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { return 0 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] != $value ? 0 : -1; } } } else { # Basic string comparisons if ($operator eq '=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' xor $value eq ''); return $$next_candidate_row->[$index] cmp $value; }; } elsif ($operator eq '<') { return sub { $$next_candidate_row->[$index] lt $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' or $value eq ''); $$next_candidate_row->[$index] le $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { $$next_candidate_row->[$index] gt $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' or $value eq ''); $$next_candidate_row->[$index] ge $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { $$next_candidate_row->[$index] ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { $$next_candidate_row->[$index] ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { $$next_candidate_row->[$index] ne $value ? 0 : -1; } } } } sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $class_name = $rule->subject_class_name; my $class_meta = $class_name->__meta__; my $rule_template = $rule->template; my $csv_column_order_names = $self->column_order; my $csv_column_count = scalar @$csv_column_order_names; my $operators_for_properties = $rule_template->operators_for_properties(); my $values_for_properties = $rule->legacy_params_hash; foreach ( values %$values_for_properties ) { if (ref eq 'HASH' and exists $_->{'value'}) { $_ = $_->{'value'}; } } my $sort_order_names = $self->sort_order; my %sort_column_names = map { $_ => 1 } @$sort_order_names; my @non_sort_column_names = grep { ! exists($sort_column_names{$_}) } @$csv_column_order_names; my %column_name_to_index_map; for (my $i = 0; $i < @$csv_column_order_names; $i++) { $column_name_to_index_map{$csv_column_order_names->[$i]} = $i; } # Index in the split-file-data for each sorted column in order my @sort_order_column_indexes = map { $column_name_to_index_map{$_} } @$sort_order_names; my(%property_meta_for_column_name); foreach my $column_name ( @$csv_column_order_names ) { my $prop = UR::Object::Property->get(class_name => $class_name, column_name => $column_name); our %WARNED_ABOUT_COLUMN; unless ( $prop or $WARNED_ABOUT_COLUMN{$class_name . '::' . $column_name}++) { $self->warning_message("Couldn't find a property in class $class_name that goes with column $column_name"); next; } $property_meta_for_column_name{$column_name} = $prop; } my @rule_columns_in_order; # The order we should perform rule matches on - value is the index in @next_file_row to test my @comparison_for_column; # closures to call to perform the match - same order as @rule_columns_in_order my $last_sort_column_in_rule = -1; # Last index in @rule_columns_in_order that applies when trying "the shortcut" my $looking_for_sort_columns = 1; my $next_candidate_row; # This will be filled in by the closure below foreach my $column_name ( @$sort_order_names, @non_sort_column_names ) { my $property_meta = $property_meta_for_column_name{$column_name}; unless ($property_meta) { Carp::croak("Class $class_name has no property connected to column named '$column_name' in data source ".$self->id); } my $property_name = $property_meta->property_name; if (! $operators_for_properties->{$property_name}) { $looking_for_sort_columns = 0; next; } elsif ($looking_for_sort_columns && $sort_column_names{$column_name}) { $last_sort_column_in_rule++; } else { # There's been a gap in the ID column list in the rule, stop looking for # further ID columns $looking_for_sort_columns = 0; } push @rule_columns_in_order, $column_name_to_index_map{$column_name}; my $operator = $operators_for_properties->{$property_name}; my $rule_value = $values_for_properties->{$property_name}; my $comparison_function = $self->_comparator_for_operator_and_property($property_meta, \$next_candidate_row, $column_name_to_index_map{$column_name}, $operator, $rule_value); unless ($comparison_function) { Carp::croak("Unknown operator '$operator' in file data source filter"); } push @comparison_for_column, $comparison_function; } my $split_regex = $self->_regex(); # FIXME - another performance boost might be to do some kind of binary search # against the file to set the initial/next position? my $file_pos = 0; # search in the offset cache for something helpful my $offset_cache = $self->_offset_cache(); # If the rule doesn't touch the sorted columns, then we can't use the offset cache for help :( if ($last_sort_column_in_rule >= 0) { # Starting at index 1 because we're interested in the file and seek data, not if it's in use # offset 0 is the in-use flag, offset 1 is a ref to the file data and offset 2 is the file seek pos SEARCH_CACHE: for (my $i = 1; $i < @$offset_cache; $i+=3) { next unless (defined($offset_cache->[$i]) && defined($offset_cache->[$i+1])); $next_candidate_row = $offset_cache->[$i]; my $matched = 0; COMPARE_VALUES: for (my $c = 0; $c <= $last_sort_column_in_rule; $c++) { my $comparison = $comparison_for_column[$c]->(); next SEARCH_CACHE if $comparison > 0; if ($comparison < 0) { $matched = 1; last COMPARE_VALUES; } } # If we made it this far, then the file data in this slot is earlier in the file # than the data we're looking for. So, if the seek pos data is later than what # we've found yet, use it instead if ($matched and $offset_cache->[$i+1] > $file_pos) { $file_pos = $offset_cache->[$i+1]; } } } my($monitor_start_time,$monitor_printed_first_fetch); if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); $monitor_printed_first_fetch = 0; my @filters_list; for (my $i = 0; $i < @rule_columns_in_order; $i++) { my $column = $rule_columns_in_order[$i]; my $column_name = $csv_column_order_names->[$column]; my $is_sorted = $i <= $last_sort_column_in_rule ? ' (sorted)' : ''; my $operator = $operators_for_properties->{$column_name} || '='; my $rule_value = $values_for_properties->{$column_name}; if (ref $rule_value eq 'ARRAY') { $rule_value = '[' . join(',', @$rule_value) . ']'; } my $filter_string = $column_name . " $operator $rule_value" . $is_sorted; push @filters_list, $filter_string; } my $filter_list = join("\n\t", @filters_list); UR::DBI->sql_fh->printf("\nFILE: %s\nFILTERS %s\n\n", $self->server, $filter_list); } $self->{'_last_read_fingerprint'} ||= ''; my $record_separator = $self->record_separator; my $cache_slot = $self->_allocate_offset_cache_slot(); my $cache_insert_counter = 100; # a "breadcrumb" will be left in the offset cache after this many lines are read my $lines_read = 0; my $printed_first_match = 0; my $lines_matched = 0; my $fh; # File handle we'll be reading from my $read_fingerprint; # The stringified version of $iterator (to avoid circular references), filled in below my $iterator = sub { unless (ref($fh)) { $fh = $self->get_default_handle(); # Lock the file for reading... For more fine-grained locking we could move this to # after READ_LINE_FROM_FILE: but that would slow down read operations a bit. If # there ends up being a problem with lock contention, go ahead and move it before $line = <$fh>; #flock($fh,LOCK_SH); } if ($monitor_start_time && ! $monitor_printed_first_fetch) { UR::DBI->sql_fh->printf("FILE: FIRST FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); $monitor_printed_first_fetch = 1; } if ($self->{'_last_read_fingerprint'} ne $read_fingerprint) { UR::DBI->sql_fh->printf("FILE: Resetting file position to $file_pos\n") if $ENV{'UR_DBI_MONITOR_SQL'}; # The last read was from a different request, reset the position $fh->seek($file_pos,0); if ($file_pos == 0) { my $skip = $self->skip_first_line; while ($skip-- > 0) { scalar(<$fh>); } } $file_pos = $self->_file_position(); $self->{'_last_read_fingerprint'} = $read_fingerprint; } local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $line; READ_LINE_FROM_FILE: until($line) { # Hack for OSX 10.5. # At EOF, the getline below will return undef. Most builds of Perl # will also set $! to 0 at EOF so you can distinguish between the cases # of EOF (which may have actually happened a while ago because of buffering) # and an actual read error. OSX 10.5's Perl does not, and so $! # retains whatever value it had after the last failed syscall, likely # a stat() while looking for a Perl module. This should have no effect # other platforms where you can't trust $! at arbitrary points in time # anyway $! = 0; $line = <$fh>; unless (defined $line) { if ($!) { redo READ_LINE_FROM_FILE if ($! == EAGAIN or $! == EINTR); my $pathname = $self->server(); Carp::confess("getline() failed for DataSource $self pathname $pathname boolexpr $rule: $!"); } # at EOF. Close up shop and return #flock($fh,LOCK_UN); $fh = undef; if ($monitor_start_time) { UR::DBI->sql_fh->printf("FILE: at EOF\nFILE: $lines_read lines read for this request. $lines_matched matches\nFILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return; } $lines_read++; my $last_read_size = length($line); chomp $line; # FIXME - to support record-oriented files, we need some replacement for this... $next_candidate_row = [ split($split_regex, $line, $csv_column_count) ]; $#{$a} = $csv_column_count-1; $file_pos = $self->_file_position(); my $file_pos_before_read = $file_pos - $last_read_size; # Every so many lines read, leave a breadcrumb about what we've seen unless ($lines_read % $cache_insert_counter) { $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; $self->_free_offset_cache_slot($cache_slot); # get a new slot $cache_slot = $self->_allocate_offset_cache_slot(); $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; $cache_insert_counter <<= 2; # Double the insert counter } for (my $i = 0; $i < @rule_columns_in_order; $i++) { my $comparison = $comparison_for_column[$i]->(); if ($comparison > 0 and $i <= $last_sort_column_in_rule) { # We've gone past the last thing that could possibly match if ($monitor_start_time) { UR::DBI->sql_fh->printf("FILE: $lines_read lines read for this request. $lines_matched matches\nFILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } #flock($fh,LOCK_UN); # Save the info from the last row we read $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; return; } elsif ($comparison) { # comparison didn't match, read another line from the file redo READ_LINE_FROM_FILE; } # That comparison worked... stay in the for() loop for other comparisons } # All the comparisons return '0', meaning they passed # Now see if the offset cache file data is different than the row we just read COMPARE_TO_CACHE: foreach my $column ( @sort_order_column_indexes) { no warnings 'uninitialized'; if ($offset_cache->[$cache_slot+1]->[$column] ne $next_candidate_row->[$column]) { # They're different. Update the offset cache data $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; last COMPARE_TO_CACHE; } } if (! $printed_first_match and $monitor_start_time) { UR::DBI->sql_fh->printf("FILE: First match after reading $lines_read lines\n"); $printed_first_match=1; } $lines_matched++; return $next_candidate_row; } }; # end sub $iterator $read_fingerprint = $iterator . ''; Sub::Name::subname('UR::DataSource::File::__datasource_iterator(closure)__', $iterator); my $count = $self->_open_query_count() || 0; $self->_open_query_count($count+1); bless $iterator, 'UR::DataSource::File::Tracker'; $iterator_data_source{$iterator} = $self; $iterator_cache_slot_refs{$iterator} = \$cache_slot; return $iterator; } sub UR::DataSource::File::Tracker::DESTROY { my $iterator = shift; my $ds = delete $iterator_data_source{$iterator}; return unless $ds; # The data source may have gone out of scope first during global destruction my $cache_slot_ref = delete $iterator_cache_slot_refs{$iterator}; if (defined($cache_slot_ref) and defined($$cache_slot_ref)) { # Mark this slot unused #print STDERR "Freeing cache slot $cache_slot\n"; #$ds->_offset_cache->[$$cache_slot_ref] = 0; $ds->_free_offset_cache_slot($$cache_slot_ref); } my $count = $ds->_open_query_count(); $ds->_open_query_count(--$count); return unless ($ds->quick_disconnect); if ($count == 0 && $ds->has_default_handle) { # All open queries have supposedly been fulfilled. Close the # file handle and undef it so get_default_handle() will re-open if necessary my $fh = $ds->get_default_handle; UR::DBI->sql_fh->printf("FILE: CLOSING fileno ".fileno($fh)."\n") if ($ENV{'UR_DBI_MONITOR_SQL'}); #flock($fh,LOCK_UN); $fh->close(); $ds->__invalidate_get_default_handle__; } } # Names of creation params that we should force to be listrefs our %creation_param_is_list = map { $_ => 1 } qw( column_order file_list sort_order constant_values ); sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; # User didn't specify columns in the file. Assumme every property is a column, and in the same order unless (exists $ds_data->{'column_order'}) { Carp::croak "data_source has no column_order specified"; } $ds_data->{'server'} ||= $ds_data->{'path'} || $ds_data->{'file'}; my %ds_creation_params; foreach my $param ( qw( delimiter record_separator column_order skip_first_line server file_list sort_order constant_values ) ) { if (exists $ds_data->{$param}) { if ($creation_param_is_list{$param} and ref($ds_data->{$param}) ne 'ARRAY') { $ds_creation_params{$param} = \( $ds_data->{$param} ); } else { $ds_creation_params{$param} = $ds_data->{$param}; } } } my($namespace, $class_name) = ($class_data->{'class_name'} =~ m/^(\w+?)::(.*)/); my $ds_id = "${namespace}::DataSource::${class_name}"; my $ds_type = delete $ds_data->{'is'}; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id ); return $ds; } # The string used to join fields of a row together # # Since the 'delimiter' property is interpreted as a regex in the reading # code, we'll try to be smart about making a real string from that. # # subclasses can override this to provide a different implementation sub join_pattern { my $self = shift; my $join_pattern = $self->delimiter; # make some common substitutions... if ($join_pattern eq '\s*,\s*') { # The default... return ', '; } $join_pattern =~ s/\\s*//g; # Turn 0-or-more whitespaces to nothing $join_pattern =~ s/\\t/\t/; # tab $join_pattern =~ s/\\s/ /; # whitespace return $join_pattern; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $read_fh = $self->get_default_handle(); unless ($read_fh) { Carp::croak($self->class . ": Can't _sync_database(): Can't open file " . $self->server . " for reading: $!"); } my $original_data_file = $self->server; my $original_data_dir = File::Basename::dirname($original_data_file); my $use_quick_rename; unless (-d $original_data_dir){ File::Path::mkpath($original_data_dir); } if (-w $original_data_dir) { $use_quick_rename = 1; # We can write to the data dir } elsif (! -w $original_data_file) { $self->error_message("Neither the directory nor the file for $original_data_file are writable - cannot sync_database"); return; } my $split_regex = $self->_regex(); my $join_pattern = $self->join_pattern; my $record_separator = $self->record_separator; local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $csv_column_order_names = $self->column_order; my $csv_column_count = scalar(@$csv_column_order_names); my %column_name_to_index_map; for (my $i = 0; $i < @$csv_column_order_names; $i++) { $column_name_to_index_map{$csv_column_order_names->[$i]} = $i; } my $changed_objects = delete $params{changed_objects}; # We're going to assumme all the passed-in objects are of the same class *gulp* my $class_name = $changed_objects->[0]->class; my $class_meta = UR::Object::Type->get(class_name => $class_name); my %column_name_to_property_meta = map { $_->column_name => $_ } grep { $_->column_name } $class_meta->all_property_metas; my @property_names_in_column_order; foreach my $column_name ( @$csv_column_order_names ) { my $prop_meta = $column_name_to_property_meta{$column_name}; unless ($prop_meta) { die "Data source " . $self->class . " id " . $self->id . " could not resolve a $class_name property for the data source's column named $column_name"; } push @property_names_in_column_order, $prop_meta->property_name; } my $insert = []; my $update = {}; my $delete = {}; foreach my $obj ( @$changed_objects ) { if ($obj->isa('UR::Object::Ghost')) { # This should be removed from the file my $original = $obj->{'db_committed'}; my $line = join($join_pattern, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } elsif ($obj->{'db_committed'}) { # This object is changed since it was read in the file my $original = $obj->{'db_committed'}; my $original_line = join($join_pattern, @{$original}{@property_names_in_column_order}) . $record_separator; my $changed_line = join($join_pattern, @{$obj}{@property_names_in_column_order}) . $record_separator; $update->{$original_line} = $changed_line; } else { # This object is new and should be added to the file push @$insert, [ @{$obj}{@property_names_in_column_order} ]; } } my $sort_order_names = $self->sort_order; foreach my $sort_column_name ( @$sort_order_names ) { unless (exists $column_name_to_index_map{$sort_column_name}) { Carp::croak("Column name '$sort_column_name' appears in the sort_order list, but not in the column_order list for data source ".$self->id); } } my $file_is_sorted = scalar(@$sort_order_names); my %column_sorts_numerically = map { $_->column_name => $_->is_numeric } values %column_name_to_property_meta; my $row_sort_sub = sub ($$) { my $comparison; foreach my $column_name ( @$sort_order_names ) { my $i = $column_name_to_index_map{$column_name}; if ($column_sorts_numerically{$column_name}) { $comparison = $_[0]->[$i] <=> $_[1]->[$i]; } else { $comparison = $_[0]->[$i] cmp $_[1]->[$i]; } return $comparison if $comparison != 0; } return 0; }; if ($sort_order_names && $file_is_sorted && scalar(@$insert)) { # the inserted things should be sorted the same way as the file my @sorted = sort $row_sort_sub @$insert; $insert = \@sorted; } my $write_fh; my $temp_file_name; if ($use_quick_rename) { $temp_file_name = sprintf("%s/.%d.%d" , $original_data_dir, time(), $$); $write_fh = IO::File->new($temp_file_name, O_WRONLY|O_CREAT); } else { $write_fh = File::Temp->new(UNLINK => 1); $temp_file_name = $write_fh->filename if ($write_fh); } unless ($write_fh) { Carp::croak "Can't create temporary file for writing: $!"; } my $monitor_start_time; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); my $time = time(); UR::DBI->sql_fh->printf("\nFILE: SYNC_DATABASE AT %d [%s]. Started transaction for %s to temp file %s\n", $time, scalar(localtime($time)), $original_data_file, $temp_file_name); } unless (flock($read_fh,LOCK_SH)) { unless ($! == EOPNOTSUPP ) { Carp::croak($self->class(). ": Can't get exclusive lock for file ".$self->server.": $!"); } } # write headers to the new file for (my $i = 0; $i < $self->skip_first_line; $i++) { my $line = <$read_fh>; $write_fh->print($line); } my $line; READ_A_LINE: while(1) { unless ($line) { $line = <$read_fh>; last unless defined $line; } if ($file_is_sorted && scalar(@$insert)) { # there are sorted things waiting to insert my $chomped = $line; chomp $chomped; my $row = [ split($split_regex, $chomped, $csv_column_count) ]; my $comparison = $row_sort_sub->($row, $insert->[0]); if ($comparison > 0) { # write the object's data no warnings 'uninitialized'; # Some of the object's data may be undef my $new_row = shift @$insert; my $new_line = join($join_pattern, @$new_row) . $record_separator; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("INSERT >>$new_line<<\n"); } $write_fh->print($new_line); # Don't undef the last line read, meaning it could still be written to the output... next READ_A_LINE; } } if (my $obj = delete $delete->{$line}) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("DELETE >>$line<<\n"); } $line = undef; next; } elsif (my $changed = delete $update->{$line}) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("UPDATE replace >>$line<< with >>$changed<<\n"); } $write_fh->print($changed); $line = undef; next; } else { # This line from the file was unchanged in the app $write_fh->print($line); $line = undef; } } if (keys %$delete) { $self->warning_message("There were ",scalar(keys %$delete)," deleted $class_name objects that did not match data in the file"); } if (keys %$update) { $self->warning_message("There were ",scalar(keys %$update)," updated $class_name objects that did not match data in the file"); } # finish out by writing the rest of the new data foreach my $new_row ( @$insert ) { no warnings 'uninitialized'; # Some of the object's data may be undef my $new_line = join($join_pattern, @$new_row) . $record_separator; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("INSERT >>$new_line<<\n"); } $write_fh->print($new_line); } $write_fh->close(); if ($use_quick_rename) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("FILE: COMMIT rename $temp_file_name over $original_data_file\n"); } unless(rename($temp_file_name, $original_data_file)) { $self->error_message("Can't rename the temp file over the original file: $!"); return; } } else { # We have to copy the data from the temp file to the original file if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("FILE: COMMIT write over $original_data_file in place\n"); } my $new_write_fh = IO::File->new($original_data_file, O_WRONLY|O_TRUNC); unless ($new_write_fh) { $self->error_message("Can't open $original_data_file for writing: $!"); return; } my $temp_file_fh = IO::File->new($temp_file_name); unless ($temp_file_fh) { $self->error_message("Can't open $temp_file_name for reading: $!"); return; } while(<$temp_file_fh>) { $new_write_fh->print($_); } $new_write_fh->close(); } # Because of the rename/copy process during syncing, the previously opened filehandle may # not be valid anymore. get_default_handle will reopen the file next time it's needed $self->_invalidate_cache(); $self->__invalidate_get_default_handle__; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->printf("FILE: TOTAL COMMIT TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } flock($read_fh, LOCK_UN); $read_fh->close(); # FIXME - this is ugly... With RDBMS-type data sources, they will call $dbh->commit() which # gets to UR::DBI->commit(), which calls _set_object_saved_committed for them. Since we're # not using DBI we have to do this 2-part thing ourselves. In the future, we might break # out things so the saving to the temp file goes in _sync_database(), and moving the temp # file over the original goes in commit() unless ($self->_set_specified_objects_saved_uncommitted($changed_objects)) { Carp::croak("Error setting objects to a saved state after sync_database. Exiting."); return; } $self->_set_specified_objects_saved_committed($changed_objects); return 1; } sub initializer_should_create_column_name_for_class_properties { 1; } 1; =pod =head1 NAME UR::DataSource::File - Parent class for file-based data sources =head1 DEPRECATED This module is deprecated. Use UR::DataSource::Filesystem instead. =head1 SYNOPSIS package MyNamespace::DataSource::MyFile; class MyNamespace::DataSource::MyFile { is => ['UR::DataSource::File', 'UR::Singleton'], }; sub server { '/path/to/file' } sub delimiter { "\t" } sub column_order { ['thing_id', 'thing_name', 'thing_color' ] } sub sort_order { ['thing_id'] } package main; class MyNamespace::Thing { id_by => 'thing_id', has => [ 'thing_id', 'thing_name', 'thing_color' ], data_source => 'MyNamespace::DataSource::MyFile', } my @objs = MyNamespace::Thing->get(thing_name => 'Bob'); =head1 DESCRIPTION Classes which wish to retrieve their data from a regular file can use a UR::DataSource::File-based data source. The modules implementing these data sources live under the DataSource subdirectory of the application's Namespace, by convention. Besides defining a class for your data source inheriting from UR::DataSource::File, it should have the following methods, either as properties or functions in the package. =head2 Configuration These methods determine the configuration for your data source. =over 4 =item server() server() should return a string representing the pathname of the file where the data is stored. =item file_list() The file_list() method should return a listref of pathnames to one or more identical files where data is stored. Use file_list() instead of server() when you want to load-balance several NFS servers, for example. You must have either server() or file_list() in your module, but not both. The existence of server() takes precedence over file_list(). =item delimiter() delimiter() should return a string representing how the fields in each record are split into columns. This string is interpreted as a regex internally. The default delimiter is "\s*,\s*" meaning that the file is separated by commas. =item record_separator() record_separator() should return a string that gets stored in $/ before getline() is called on the file's filehandle. The default record_separator() is "\n" meaning that the file's records are separated by newlines. =item skip_first_line() skip_first_line() should return a boolean value. If true, the first line of the file is ignored, for example if the first line defines the columns in the file. =item column_order() column_order() should return a listref of column names in the file. column_order is required; there is no default. =item sort_order() If the data file is sorted in some way, sort_order() should return a listref of column names (which must exist in column_order()) by which the file is sorted. This gives the system a hint about how the file is structured, and is able to make shortcuts when reading the file to speed up data access. The default is to assumme the file is not sorted. =back =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource =cut FileMux.pm100664023532023421 7466612544604516 16735 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::FileMux; # NOTE! This module is deprecated. Use UR::DataSource::Filesystem instead. use UR; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; class UR::DataSource::FileMux { is => ['UR::DataSource'], doc => 'A factory for other datasource factories that is able to pivot depending on parameters in the rule used for get()', has => [ delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, column_order => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, cache_size => { is => 'Integer', default_value => 100 }, skip_first_line => { is => 'Integer', default_value => 0 }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, quick_disconnect => { is => 'Boolean', default_value => 1, doc => 'Do not hold the file handle open between requests' }, file_resolver => { is => 'CODE', doc => 'subref that will return a pathname given a rule' }, constant_values => { is => 'ARRAY', default_value => undef, doc => 'Property names which are not in the data file(s), but are part of the objects loaded from the data source' }, ], has_optional => [ server => { is => 'String', doc => 'pathname to the data file' }, file_list => { is => 'ARRAY', doc => 'list of pathnames of equivalent files' }, sort_order => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, required_for_get => { is => 'ARRAY', doc => 'Property names which must appear in any get() request using this data source. It is used to build the argument list for the file_resolver sub' }, delegate_file_ds => { is => 'UR:DataFile::FileMuxFile', reverse_as => 'controlling_filemux', is_many => 1 }, ], }; UR::Object::Type->define( class_name => 'UR::DataSource::FileMuxFile', is => 'UR::DataSource::File', has_transient => [ controlling_filemux => { is => 'UR::DataSource::FileMux', id_by => 'controlling_filemux_id' }, ], )->is_uncachable(1); # FileMux doesn't have a 'default_handle' sub create_default_handle { return undef; } sub disconnect { my $self = shift; my @delegates = $self->delegate_file_ds(); $_->disconnect_default_handle foreach @delegates; } # The concreate data sources will be of this type sub _delegate_data_source_class { 'UR::DataSource::FileMuxFile'; } sub sql_fh { return UR::DBI->sql_fh(); } sub can_savepoint { 0;} # Doesn't support savepoints my %WORKING_RULES; # Avoid recusion when infering values from rules sub create_iterator_closure_for_rule { my($self,$rule) = @_; if ($WORKING_RULES{$rule->id}++) { my $subject_class = $rule->subject_class_name; $self->error_message("Recursive entry into create_iterator_closure_for_rule() for class $subject_class rule_id ".$rule->id); $WORKING_RULES{$rule->id}--; return; } my $context = UR::Context->get_current; my $required_for_get = $self->required_for_get; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->printf("FILEMux: Resolving values for %d params (%s)\n", scalar(@$required_for_get), join(',',@$required_for_get)); } my @all_resolver_params; for(my $i = 0; $i < @$required_for_get; $i++) { my $param_name = $required_for_get->[$i]; my @values = $context->infer_property_value_from_rule($param_name, $rule); unless (@values) { # Hack: the above infer...rule() returned 0 objects, so $all_params_loaded made # a note of it. Later on, if the user supplies more params such that it would be # able to resolve a file, we'll never get here, because the Context will see that a # superset of the params (this current invocation without sufficient params) was already # tried and results should be entirely in the cache - ie. no objects. # So... remove the evidence that we tried this in case the user is catching the die # below and will continue on $context->_forget_loading_was_done_with_template_and_rule($rule->template_id, $rule->id); Carp::croak "Can't resolve data source: no $param_name specified in rule $rule"; } if (@values == 1 and ref($values[0]) eq 'ARRAY') { @values = @{$values[0]}; } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print(" FILEMux: $param_name: (",join(',',@values),")\n"); } unless ($rule->specifies_value_for($param_name)) { if (scalar(@values) == 1) { $rule = $rule->add_filter($param_name => $values[0]); } else { $rule = $rule->add_filter($param_name => \@values); } } $all_resolver_params[$i] = \@values; } my @resolver_param_combinations = UR::Util::combinations_of_values(@all_resolver_params); # Each combination of params ends up being from a different data source. Make an # iterator pulling from each of them my $file_resolver = $self->{'file_resolver'}; if (ref($file_resolver) ne 'CODE') { # Hack! The data source is probably a singleton class and there's a file_resolver method # defined $file_resolver = $self->can('file_resolver'); } my $concrete_ds_type = $self->_delegate_data_source_class; #my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); my @constant_value_properties = @{$self->constant_values}; my @data_source_construction_data; foreach my $resolver_params ( @resolver_param_combinations ) { push @data_source_construction_data, { subject_class_name => $rule->subject_class_name, file_resolver => $file_resolver, file_resolver_params => $resolver_params, }; } delete $WORKING_RULES{$rule->id}; my($monitor_start_time,$monitor_printed_first_fetch); if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); $monitor_printed_first_fetch = 0; } my $base_sub_ds_name = $self->id; # Fill in @ds_iterators with iterators for all the underlying data sources # pre-fill @ds_next_row with the next object from each data source # @ds_constant_values is the constant_values for objects of those data sources my(@ds_iterators, @ds_next_row, @ds_constant_values); foreach my $data_source_construction_data ( @data_source_construction_data ) { my $subject_class_name = $data_source_construction_data->{'subject_class_name'}; my $file_resolver = $data_source_construction_data->{'file_resolver'}; my $file_resolver_params = $data_source_construction_data->{'file_resolver_params'}; my @sub_ds_name_parts; my $this_ds_rule_params = $rule->legacy_params_hash; for (my $i = 0; $i < @$required_for_get; $i++) { my $param_name = $required_for_get->[$i]; my $param_value = $file_resolver_params->[$i]; push @sub_ds_name_parts, $param_name . $param_value; $this_ds_rule_params->{$param_name} = $param_value; } my $sub_ds_id = join('::', $base_sub_ds_name, @sub_ds_name_parts); my $resolved_file = $file_resolver->(@$file_resolver_params); unless ($resolved_file) { Carp::croak "Can't create data source: file resolver for $sub_ds_id returned false for params " . join(',',@$file_resolver_params); } my $this_ds_obj = $self->get_or_create_data_source($concrete_ds_type, $sub_ds_id, $resolved_file); my $this_ds_rule = UR::BoolExpr->resolve($subject_class_name,%$this_ds_rule_params); my @constant_values = map { $this_ds_rule->value_for($_) } @constant_value_properties; my $ds_iterator = $this_ds_obj->create_iterator_closure_for_rule($this_ds_rule); my $initial_obj = $ds_iterator->(); next unless $initial_obj; push @ds_constant_values, \@constant_values; push @ds_iterators, $ds_iterator; push @ds_next_row, $initial_obj; } unless (scalar(@ds_constant_values) == scalar(@ds_iterators) and scalar(@ds_constant_values) == scalar(@ds_next_row) ) { Carp::croak("Internal error in UR::DataSource::FileMux: arrays for iterators, constant_values and next_row have differing sizes"); } # Create a closure that can sort the next possible rows in @ds_next_row and return the index of # the one that sorts earliest my $sorter; if (@ds_iterators == 0 ) { # No underlying data sources, no data to return return sub {}; } elsif (@ds_iterators == 1 ) { # Only one underlying data source. $sorter = sub { 0 }; } else { # more than one underlying data source, make a real sorter my %column_name_to_row_index; my $column_order_names = $self->column_order; my $constant_values = $self->constant_values; push @$column_order_names, @$constant_values; for (my $i = 0; $i < @$column_order_names; $i++) { $column_name_to_row_index{$column_order_names->[$i]} = $i; } my $sort_order = $self->sort_order; if (! $sort_order or ! @$sort_order ) { # They didn't specify sorting, Try finding out the class' ID properties # and sort by them my $subject_class_meta = $rule->subject_class_name->__meta__; my @id_properties = $subject_class_meta->direct_id_property_names; $sort_order = []; foreach my $property_name ( @id_properties ) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); my $column_name = $property_meta->column_name; next unless $column_name; next unless ($column_name_to_row_index{$column_name}); push @$sort_order, $column_name; } } my @row_index_sort_order = map { $column_name_to_row_index{$_} } @$sort_order; $sorter = sub { my $lowest_obj_idx = 0; COMPARE_OBJECTS: for(my $compare_obj_idx = 1; $compare_obj_idx < @ds_next_row; $compare_obj_idx++) { COMPARE_COLUMNS: for (my $i = 0; $i < @row_index_sort_order; $i++) { my $column_num = $row_index_sort_order[$i]; my $comparison = $ds_next_row[$lowest_obj_idx]->[$column_num] <=> $ds_next_row[$compare_obj_idx]->[$column_num] || $ds_next_row[$lowest_obj_idx]->[$column_num] cmp $ds_next_row[$compare_obj_idx]->[$column_num]; if ($comparison == -1) { next COMPARE_OBJECTS; } elsif ($comparison == 1) { $lowest_obj_idx = $compare_obj_idx; next COMPARE_OBJECTS; } } } return $lowest_obj_idx; }; } my $iterator = sub { if ($monitor_start_time and ! $monitor_printed_first_fetch) { $self->sql_fh->printf("FILEMux: FIRST FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); $monitor_printed_first_fetch = 1; } while (@ds_next_row) { my $next_row_idx = $sorter->(); my $next_row_to_return = $ds_next_row[$next_row_idx]; push @$next_row_to_return, @{$ds_constant_values[$next_row_idx]}; my $refill_row = $ds_iterators[$next_row_idx]->(); if ($refill_row) { $ds_next_row[$next_row_idx] = $refill_row; } else { # This iterator is exhausted splice(@ds_iterators, $next_row_idx, 1); splice(@ds_constant_values, $next_row_idx, 1); splice(@ds_next_row, $next_row_idx, 1); } return $next_row_to_return; } if ($monitor_start_time) { $self->sql_fh->printf("FILEMux: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return; }; Sub::Name::subname('UR::DataSource::FileMux::__datasource_iterator(closure)__', $iterator); return $iterator; } sub get_or_create_data_source { my($self, $concrete_ds_type, $sub_ds_id, $file_path) = @_; my $sub_ds; unless ($sub_ds = $concrete_ds_type->get($sub_ds_id)) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print("FILEMux: $file_path is data source $sub_ds_id\n"); } my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); $concrete_ds_type->define( id => $sub_ds_id, %sub_ds_params, server => $file_path, controlling_filemux_id => $self->id, ); $UR::Context::all_objects_cache_size++; $sub_ds = $concrete_ds_type->get($sub_ds_id); unless ($sub_ds) { Carp::croak "Can't create data source: retrieving newly defined data source $sub_ds_id returned nothing"; } # Since these $sub_ds objects have no data_source, this will indicate to # UR::Context::prune_object_cache() that it's ok to go ahead and drop them $sub_ds->__weaken__(); } return $sub_ds; } sub _generate_loading_templates_arrayref { my $self = shift; my $delegate_class = $self->_delegate_data_source_class(); $delegate_class->class; # trigger the autoloader, if necessary my $sub = $delegate_class->can('_generate_loading_templates_arrayref'); unless ($sub) { Carp::croak(qq(FileMux can't locate method "_generate_loading_templates_arrayref" via package $delegate_class. Is $delegate_class a File-type DataSource?)); } $self->$sub(@_); } sub _normalize_file_resolver_details { my($class, $class_data, $ds_data) = @_; my $path_resolver_coderef; my @required_for_get; my $class_name = $class_data->{'class_name'}; if (exists $ds_data->{'required_for_get'}) { @required_for_get = @{$ds_data->{'required_for_get'}}; my $user_supplied_resolver = $ds_data->{'file_resolver'} || $ds_data->{'resolve_file_with'} || $ds_data->{'resolve_path_with'}; if (ref($user_supplied_resolver) eq 'CODE') { $path_resolver_coderef = $user_supplied_resolver; } elsif (! ref($user_supplied_resolver)) { # It's a functcion name $path_resolver_coderef = $class_name->can($user_supplied_resolver); unless ($path_resolver_coderef) { die "Can't locate function $user_supplied_resolver via class $class_name during creation of inline data source"; } } else { $class->error_message("The data_source specified 'required_for_get', but the file resolver was not a coderef or function name"); return; } } else { my $resolve_path_with = $ds_data->{'resolve_path_with'} || $ds_data->{'path'} || $ds_data->{'server'} || $ds_data->{'file_resolver'}; unless ($resolve_path_with or $ds_data->{'file_list'}) { $class->error_message("A data_source's definition must include 'resolve_path_with', 'path', 'server', or 'file_list'"); return; } if (! ref($resolve_path_with)) { # a simple string if ($class_name->can($resolve_path_with) or grep { $_ eq $resolve_path_with } @{$class_data->{'has'}}) { # a method or property name no strict 'refs'; $path_resolver_coderef = \&{ $class_name . "::$resolve_path_with"}; } else { # a hardcoded pathname $path_resolver_coderef = sub { $resolve_path_with }; } } elsif (ref($resolve_path_with) eq 'CODE') { $path_resolver_coderef = $resolve_path_with; } elsif (ref($resolve_path_with) ne 'ARRAY') { $class->error_message("A data_source's 'resolve_path_with' must be a coderef, arrayref, pathname or method name"); return; } elsif (ref($resolve_path_with) eq 'ARRAY') { # A list of things if (ref($resolve_path_with->[0]) eq 'CODE') { # A coderef, then property list @required_for_get = @{$ds_data->{'resolve_path_with'}}; $path_resolver_coderef = shift @required_for_get; } elsif (grep { $_ eq $resolve_path_with->[0] } keys(%{$class_data->{'has'}}) ) { # a list of property names, join them with /s unless ($ds_data->{'base_path'}) { $class->warning_message("$class_name inline data source: 'resolve_path_with' is a list of method names, but 'base_path' is undefined'"); } @required_for_get = @{$resolve_path_with}; my $base_path = $ds_data->{'base_path'}; $path_resolver_coderef = sub { no warnings 'uninitialized'; return join('/', $base_path, @_) }; } elsif ($class_name->can($resolve_path_with->[0])) { # a method compiled into the class, but not one that's a property @required_for_get = @{$resolve_path_with}; my $fcn_name = shift @required_for_get; my $path_resolver_coderef = $class_name->can($fcn_name); unless ($path_resolver_coderef) { die "Can't locate function $fcn_name via class $class_name during creation of inline data source"; } } elsif (! ref($resolve_path_with->[0])) { # treat the first element as a sprintf format @required_for_get = @{$resolve_path_with}; my $format = shift @required_for_get; $path_resolver_coderef = sub { no warnings 'uninitialized'; return sprintf($format, @_); }; } else { $class->error_message("Unrecognized layout for 'resolve_path_with'"); return; } } else { $class->error_message("Unrecognized layout for 'resolve_path_with'"); return; } } return ($path_resolver_coderef, @required_for_get); } # Properties we'll copy from $self when creating a concrete data source sub _common_params_for_concrete_data_sources { my $self = shift; my %params; foreach my $param ( qw( delimiter skip_first_line column_order sort_order record_separator constant_values handle_class quick_disconnect ) ) { next unless defined $self->$param; my @vals = $self->$param; if (@vals > 1) { $params{$param} = \@vals; } else { $params{$param} = $vals[0]; } } return %params; } sub initializer_should_create_column_name_for_class_properties { 1; } # Called by the class initializer sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; unless ($ds_data->{'column_order'}) { die "Can't create inline data source for ".$class_data->{'class_name'}.": 'column_order' is a required param"; } my($file_resolver, @required_for_get) = $class->_normalize_file_resolver_details($class_data, $ds_data); return unless $file_resolver; if (!exists($ds_data->{'constant_values'}) and @required_for_get) { # If there are required_for_get params, but the user didn't specify any constant_values, # then all the required_for_get items that are real properties become constant_values $ds_data->{'constant_values'} = []; my %columns_from_ds = map { $_ => 1 } @{$ds_data->{'column_order'}}; foreach my $param_name ( @required_for_get ) { my $param_data = $class_data->{'has'}->{$param_name}; next unless $param_data; my $param_column = $param_data->{'column_name'}; next unless $param_column; unless ($columns_from_ds{$param_column}) { push @{$ds_data->{'constant_values'}}, $param_name; } } } my %ds_creation_params; foreach my $param ( qw( delimiter record_separator column_order cache_size skip_first_line sort_order constant_values ) ) { if (exists $ds_data->{$param}) { $ds_creation_params{$param} = $ds_data->{$param}; } } my($namespace, $class_name) = ($class_data->{'class_name'} =~ m/^(\w+?)::(.*)/); my $ds_id = "${namespace}::DataSource::${class_name}"; my $ds_type = delete $ds_data->{'is'}; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id, required_for_get => \@required_for_get, file_resolver => $file_resolver ); return $ds; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $changed_objects = delete $params{'changed_objects'}; my $context = UR::Context->get_current; my $required_for_get = $self->required_for_get; my $file_resolver = $self->{'file_resolver'}; if (ref($file_resolver) ne 'CODE') { # Hack! The data source is probably a singleton class and there's a file_resolver method # defined $file_resolver = $self->can('file_resolver'); } my $monitor_start_time; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); my $time = time(); $self->sql_fh->printf("FILEMux: SYNC_DATABASE AT %d [%s].\n", $time, scalar(localtime($time))); } my $concrete_ds_type = $self->_delegate_data_source_class; my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); my %datasource_for_dsid; my %objects_by_datasource; foreach my $obj ( @$changed_objects ) { my @obj_values; for (my $i = 0; $i < @$required_for_get; $i++) { my $property = $required_for_get->[$i]; my $value = $obj->$property; unless ($value) { my $class = $obj->class; my $id = $obj->id; $self->error_message("No value for required-for-get property $property on object of class $class id $id"); return; } if (ref $value) { my $class = $obj->class; my $id = $obj->id; $self->error_message("Pivoting based on a non-scalar property is not supported. $class object id $id property $property did not return a scalar value"); return; } push @obj_values, $value; } my @sub_ds_name_parts; for (my $i = 0; $i < @obj_values; $i++) { push @sub_ds_name_parts, $required_for_get->[$i] . $obj_values[$i]; } my $sub_ds_id = join('::', $self->id, @sub_ds_name_parts); my $sub_ds = $datasource_for_dsid{$sub_ds_id} || $concrete_ds_type->get($sub_ds_id); unless ($sub_ds) { my $file_path = $file_resolver->(@obj_values); unless (defined $file_path) { die "Can't resolve data source: resolver for " . $self->class . " returned undef for params " . join(',',@obj_values); } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print("FILEMux: $file_path is data source $sub_ds_id\n"); } $concrete_ds_type->define( id => $sub_ds_id, %sub_ds_params, server => $file_path, controlling_filemux_id => $self->id, ); $UR::Context::all_objects_cache_size++; $sub_ds = $concrete_ds_type->get($sub_ds_id); # Since these $sub_ds objects have no data_source, this will indicate to # UR::Context::prune_object_cache() that it's ok to go ahead and drop them $sub_ds->__weaken__(); } unless ($sub_ds) { die "Can't get data source with ID $sub_ds_id"; } $datasource_for_dsid{$sub_ds_id} ||= $sub_ds; unless ($objects_by_datasource{$sub_ds_id}) { $objects_by_datasource{$sub_ds_id}->{'ds_obj'} = $sub_ds; $objects_by_datasource{$sub_ds_id}->{'changed_objects'} = []; } push(@{$objects_by_datasource{$sub_ds_id}->{'changed_objects'}}, $obj); } foreach my $h ( values %objects_by_datasource ) { my $sub_ds = $h->{'ds_obj'}; my $changed_objects = $h->{'changed_objects'}; $sub_ds->_sync_database(changed_objects => $changed_objects); } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->printf("FILEMux: TOTAL COMMIT TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return 1; } 1; =pod =head1 NAME UR::DataSource::FileMux - Parent class for datasources which can multiplex many files together =head1 DEPRECATED This module is deprecated. Use UR::DataSource::Filesystem instead. =head1 SYNOPSIS package MyNamespace::DataSource::MyFileMux; class MyNamespace::DataSource::MyFileMux { is => ['UR::DataSource::FileMux', 'UR::Singleton'], }; sub column_order { ['thing_id', 'thing_name', 'thing_color'] } sub sort_order { ['thing_id'] } sub delimiter { "\t" } sub constant_values { ['thing_type'] } sub required_for_get { ['thing_type'] } sub file_resolver { my $thing_type = shift; return '/base/path/to/files/' . $thing_type; } package main; class MyNamespace::ThingMux { id_by => ['thing_id', 'thing_type' ], has => ['thing_id', 'thing_type', 'thing_name','thing_color'], data_source => 'MyNamespace::DataSource::MyFileMux', }; my @objs = MyNamespace::Thing->get(thing_type => 'people', thing_name => 'Bob'); =head1 DESCRIPTION UR::DataSource::FileMux provides a framework for file-based data sources where the data files are split up between one or more parameters of the class. For example, in the synopsis above, the data for the class is stored in several files in the directory /base/path/to/files/. Each file may have a name such as 'people' and 'cars'. When a get() request is made on the class, the parameter 'thing_type' must be present in the rule, and the value of that parameter is used to complete the file's pathname, via the file_resolver() function. Note that even though the 'thing_type' parameter is not actually stored in the file, its value for the loaded objects gets filled in because that paremeter exists in the constant_values() configuration list, and in the get() request. =head2 Configuration These methods determine the configuration for your data source and should appear as properties of the data source or as functions in the package. =over 4 =item delimiter() =item record_separator() =item skip_first_line() =item column_order() =item sort_order() These configuration items behave the same as in a UR::DataSource::File-based data source. =item required_for_get() required_for_get() should return a listref of parameter names. Whenever a get() request is made on the class, the listed parameters must appear in the rule, or be derivable via UR::Context::infer_property_value_from_rule(). =item file_resolver() file_resolver() is called as a function (not a method). It should accept the same number of parameters as are mentioned in required_for_get(). When a get() request is made, those named parameters are extracted from the rule and passed in to the file_resolver() function in the same order. file_resolver() must return a string that is used as the pathname to the file that contains the needed data. The function must not have any other side effects. In the case where the data source is a regular object (not a UR::Singleton'), then the file_resover parameter should return a coderef. =item constant_values() constant_values() should return a listref of parameter names. These parameter names are used by the object loader system to fill in data that may not be present in the data files. If the class has parameters that are not actually stored in the data files, then the parameter values are extracted from the rule and stored in the loaded object instances before being returned to the user. In the synopsis above, thing_type is not stored in the data files, even though it exists as a parameter of the MyNamespace::ThingMux class. =back =head2 Theory of Operation As part of the data-loading infrastructure inside UR, the parameters in a get() request are transformed into a UR::BoolExpr instance, also called a rule. UR::DataSource::FilMux hooks into that infrastructure by implementing create_iterator_closure_for_rule(). It first collects the values for all the parameters mentioned in required_for_get() by passing the rule and needed parameter to infer_property_value_from_rule() of the current Context. If any of the needed parameters is not resolvable, an excpetion is raised. Some of the rule's parameters may have multiple values. In those cases, all the combinations of values are expanded. For example of param_a has 2 values, and param_b has 3 values, then there are 6 possible combinations. For each combination of values, the file_resolver() function is called and returns a pathname. For each pathname, a file-specific data source is created (if it does not already exist), the server() configuration parameter created to return that pathname. Other parameters are copied from the values in the FileMux data source, such as column_names and delimiter. create_iterator_closure_for_rule() is called on each of those data sources. Finally, an iterator is created to wrap all of those iterators, and is returned. =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource, UR::DataSource::File =cut Filesystem.pm100664023532023421 24002112544604516 17504 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::Filesystem; use UR; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; use File::Basename; use File::Path; use List::Util; use Scalar::Util; use Errno qw(EINTR EAGAIN EOPNOTSUPP); # lets you specify the server in several ways: # path => '/path/name' # means there is one file storing the data # path => [ '/path1/name', '/path2/name' ] # means the first tile we need to open the file, pick one (for load balancing) # path => '/path/to/directory/' # means that directory contains one or more files, and the classes using # this datasource can have table_name metadata to pick the file # path => '/path/$param1/${param2}.ext' # means the values for $param1 and $param2 should come from the input rule. # If the rule doesn't specify the param, then it should glob for the possible # names at that point in the filesystem # path => '/path/&method/filename' # means the value for that part of the path should come from a method call # run as $subject_class_name->$method($rule) # path => '/path/*/path/$name/ # means it should glob at the appropriate time for the '*', but no use the # paths found matching the glob to infer any values # maybe suppert a URI scheme like # file:/path/$to/File.ext?columns=[a,b,c]&sorted_columns=[a,b] # TODO # * Support non-equality operators for properties that are part of the path spec class UR::DataSource::Filesystem { is => 'UR::DataSource', has => [ path => { doc => 'Path spec for the path on the filesystem containing the data' }, delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, header_lines => { is => 'Integer', default_value => 0, doc => 'Number of lines at the start of the file to skip' }, columns_from_header => { is => 'Boolean', default_value => 0, doc => 'The column names are in the first line of the file' }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, ], has_optional => [ columns => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, sorted_columns => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, ], doc => 'A data source for treating files as relational data', }; sub can_savepoint { 0;} # Doesn't support savepoints # Filesystem datasources don't have a "default_handle" sub create_default_handle { undef } sub _regex { my $self = shift; unless ($self->{'_regex'}) { my $delimiter = $self->delimiter; my $r = eval { qr($delimiter) }; if ($@ || !$r) { $self->error_message("Unable to interepret delimiter '".$self->delimiter.": $@"); return; } $self->{'_regex'} = $r; } return $self->{'_regex'}; } sub _logger { my $self = shift; my $varname = shift; if ($ENV{$varname}) { my $log_fh = UR::DBI->sql_fh; return sub { my $msg = shift; my $time = time(); $msg =~ s/\b\$time\b/$time/g; my $localtime = scalar(localtime $time); $msg =~ s/\b\$localtime\b/$localtime/; $log_fh->print($msg); }; } else { return \&UR::Util::null_sub; } } # The behavior for handling the filehandles after fork is contained in # the read_record_from_file closure. There's nothing special for the # data source to do sub prepare_for_fork { return 1; } sub finish_up_after_fork { return 1; } # Like UR::BoolExpr::specifies_value_for, but works on either a BoolExpr # or another object. In the latter case, it returns true if the object's # class has the given property sub __specifies_value_for { my($self, $thing, $property_name) = @_; return $thing->isa('UR::BoolExpr') ? $thing->specifies_value_for($property_name) : $thing->__meta__->property_meta_for_name($property_name); } # Like UR::BoolExpr::value_for, but works on either a BoolExpr # or another object. sub __value_for { my($self, $thing, $property_name) = @_; return $thing->isa('UR::BoolExpr') ? $thing->value_for($property_name) : $thing->$property_name; } # Like UR::BoolExpr::subject_class_name, but works on either a BoolExpr # or another object. sub __subject_class_name { my($self, $thing) = @_; return $thing->isa('UR::BoolExpr') ? $thing->subject_class_name() : $thing->class; } sub _replace_vars_with_values_in_pathname { my($self, $rule_or_obj, $string, $prop_values_hash) = @_; $prop_values_hash ||= {}; # Match something like /some/path/$var/name or /some/path${var}.ext/name if ($string =~ m/\$\{?(\w+)\}?/) { my $varname = $1; my $subject_class_name = $self->__subject_class_name($rule_or_obj); unless ($subject_class_name->__meta__->property_meta_for_name($varname)) { Carp::croak("Invalid 'server' for data source ".$self->id . ": Path spec $string requires a value for property $varname " . " which is not a property of class $subject_class_name"); } my @string_replacement_values; if ($self->__specifies_value_for($rule_or_obj, $varname)) { my @property_values = $self->__value_for($rule_or_obj, $varname); if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # Make a listref that has one element per value for that property in the rule (in-clause # rules may have more than one value) # Each element has 2 parts, first is the value, second is the accumulated prop_values_hash # where we've added the occurance of this property havine one of the values @property_values = map { [ $_, { %$prop_values_hash, $varname => $_ } ] } @property_values; # Escape any shell glob characters in the values: [ ] { } ~ ? * and \ # we don't want a property with value '?' to be a glob wildcard @string_replacement_values = map { $_->[0] =~ s/([[\]{}~?*\\])/\\$1/; $_ } @property_values; } else { # The rule doesn't have a value for this property. # Put a shell wildcard in here, and a later glob will match things # The '.__glob_positions__' key holds a list of places we've inserted shell globs. # Each element is a 2-element list: index 0 is the string position, element 1 if the variable name. # This is needed so the later glob expansion can tell the difference between globs # that are part of the original path spec, and globs put in here my @glob_positions = @{ $prop_values_hash->{'.__glob_positions__'} || [] }; my $glob_pos = $-[0]; push @glob_positions, [$glob_pos, $varname]; @string_replacement_values = ([ '*', { %$prop_values_hash, '.__glob_positions__' => \@glob_positions} ]); } my @return = map { my $s = $string; substr($s, $-[0], $+[0] - $-[0], $_->[0]); [ $s, $_->[1] ]; } @string_replacement_values; # recursion to process the next variable replacement return map { $self->_replace_vars_with_values_in_pathname($rule_or_obj, @$_) } @return; } else { return [ $string, $prop_values_hash ]; } } sub _replace_subs_with_values_in_pathname { my($self, $rule_or_obj, $string, $prop_values_hash) = @_; $prop_values_hash ||= {}; my $subject_class_name = $self->__subject_class_name($rule_or_obj); # Match something like /some/path/&sub/name or /some/path&{sub}.ext/name if ($string =~ m/\&\{?(\w+)\}?/) { my $subname = $1; unless ($subject_class_name->can($subname)) { Carp::croak("Invalid 'server' for data source ".$self->id . ": Path spec $string requires a value for method $subname " . " which is not a method of class " . $self->__subject_class_name($rule_or_obj)); } my @property_values = eval { $subject_class_name->$subname($rule_or_obj) }; if ($@) { Carp::croak("Can't resolve final path for 'server' for data source ".$self->id . ": Method call to ${subject_class_name}::${subname} died with: $@"); } if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # Make a listref that has one element per value for that property in the rule (in-clause # rules may have more than one value) # Each element has 2 parts, first is the value, second is the accumulated prop_values_hash # where we've added the occurance of this property havine one of the values @property_values = map { [ $_, { %$prop_values_hash } ] } @property_values; # Escape any shell glob characters in the values: [ ] { } ~ ? * and \ # we don't want a return value '?' or '*' to be a glob wildcard my @string_replacement_values = map { $_->[0] =~ s/([[\]{}~?*\\])/\\$1/; $_ } @property_values; # Given a pathname returned from the glob, return a new glob_position_list # that has fixed up the position information accounting for the fact that # the globbed pathname is a different length than the original spec my $original_path_length = length($string); my $glob_position_list = $prop_values_hash->{'.__glob_positions__'}; my $subname_replacement_position = $-[0]; my $fix_offsets_in_glob_list = sub { my $pathname = shift; # alter the position only if it is greater than the position of # the subname we're replacing return map { [ $_->[0] < $subname_replacement_position ? $_->[0] : $_->[0] + length($pathname) - $original_path_length, $_->[1] ] } @$glob_position_list; }; my @return = map { my $s = $string; substr($s, $-[0], $+[0] - $-[0], $_->[0]); $_->[1]->{'.__glob_positions__'} = [ $fix_offsets_in_glob_list->($s) ]; [ $s, $_->[1] ]; } @string_replacement_values; # recursion to process the next method call return map { $self->_replace_subs_with_values_in_pathname($rule_or_obj, @$_) } @return; } else { return [ $string, $prop_values_hash ]; } } sub _replace_glob_with_values_in_pathname { my($self, $string, $prop_values_hash) = @_; # a * not preceeded by a backslash, delimited by / if ($string =~ m#([^/]*?[^\\/]?(\*)[^/]*)#) { my $glob_pos = $-[2]; my $path_segment_including_glob = substr($string, 0, $+[0]); my $remaining_path = substr($string, $+[0]); my @glob_matches = map { $_ . $remaining_path } glob($path_segment_including_glob); my $resolve_glob_values_for_each_result; my $glob_position_list = $prop_values_hash->{'.__glob_positions__'}; # Given a pathname returned from the glob, return a new glob_position_list # that has fixed up the position information accounting for the fact that # the globbed pathname is a different length than the original spec my $original_path_length = length($string); my $fix_offsets_in_glob_list = sub { my $pathname = shift; return map { [ $_->[0] + length($pathname) - $original_path_length, $_->[1] ] } @$glob_position_list; }; if ($glob_position_list->[0]->[0] == $glob_pos) { # This * was put in previously by a $propname in the spec that wasn't mentioned in the rule my $path_delim_pos = index($path_segment_including_glob, '/', $glob_pos); $path_delim_pos = length($path_segment_including_glob) if ($path_delim_pos == -1); # No more /s my $regex_as_str = $path_segment_including_glob; # Find out just how many *s we're dealing with and where they are, up to the next / # remove them from the glob_position_list because we're going to resolve their values my(@glob_positions, @property_names); while (@$glob_position_list and $glob_position_list->[0]->[0] < $path_delim_pos ) { my $this_glob_info = shift @{$glob_position_list}; push @glob_positions, $this_glob_info->[0]; push @property_names, $this_glob_info->[1]; } # Replace the *s found with regex captures my $glob_replacement = '([^/]*)'; my $glob_rpl_offset = 0; my $offset_inc = length($glob_replacement) - 1; # replacing a 1-char string '*' with a 7-char string '([^/]*)' $regex_as_str = List::Util::reduce( sub { substr($a, $b + $glob_rpl_offset, 1, $glob_replacement); $glob_rpl_offset += $offset_inc; $a; }, ($regex_as_str, @glob_positions) ); my $regex = qr{$regex_as_str}; my @property_values_for_each_glob_match = map { [ $_, [ $_ =~ $regex] ] } @glob_matches; # Fill in the property names into .__glob_positions__ # we've resolved in this iteration, and apply offset fixups for the # difference in string length between the pre- and post-glob pathnames $resolve_glob_values_for_each_result = sub { return map { my %h = %$prop_values_hash; @h{@property_names} = @{$_->[1]}; $h{'.__glob_positions__'} = [ $fix_offsets_in_glob_list->($_->[0]) ]; [$_->[0], \%h]; } @property_values_for_each_glob_match; }; } else { # This is a glob put in the original path spec # The new path comes from the @glob_matches list. # Apply offset fixups for the difference in string length between the # pre- and post-glob pathnames $resolve_glob_values_for_each_result = sub { return map { [ $_, { %$prop_values_hash, '.__glob_positions__' => [ $fix_offsets_in_glob_list->($_) ] } ] } @glob_matches; }; } my @resolved_paths_and_property_values = $resolve_glob_values_for_each_result->(); # Recursion to process the next glob return map { $self->_replace_glob_with_values_in_pathname( @$_ ) } @resolved_paths_and_property_values; } else { delete $prop_values_hash->{'.__glob_positions__'}; return [ $string, $prop_values_hash ]; } } sub resolve_file_info_for_rule_and_path_spec { my($self, $rule, $path_spec) = @_; $path_spec ||= $self->path; return map { $self->_replace_glob_with_values_in_pathname(@$_) } map { $self->_replace_subs_with_values_in_pathname($rule, @$_) } $self->_replace_vars_with_values_in_pathname($rule, $path_spec); } # We're overriding path() so the first time it's called, it will # pick one from the list and then stay with that one for the life # of the program sub path { my $self = shift; unless ($self->{'__cached_path'}) { my $path = $self->__path(); if (ref($path) and ref($path) eq 'ARRAY') { my $count = @$path; my $idx = $$ % $count; $self->{'_cached_path'} = $path->[$idx]; } else { $self->{'_cached_path'} = $path; } } return $self->{'_cached_path'}; } # Names of creation params that we should force to be listrefs our %creation_param_is_list = map { $_ => 1 } qw( columns sorted_columns ); sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; #unless (exists $ds_data->{'columns'}) { # User didn't specify columns in the file. Assumme every property is a column, and in the same order # We'll have to ask the class object for the column list the first time there's a query #} my %ds_creation_params; foreach my $param ( qw( path delimiter record_separator columns header_lines columns_from_header handle_class sorted_columns ) ) { if (exists $ds_data->{$param}) { if ($creation_param_is_list{$param} and ref($ds_data->{$param}) ne 'ARRAY') { $ds_creation_params{$param} = \( $ds_data->{$param} ); } else { $ds_creation_params{$param} = $ds_data->{$param}; } } } my $ds_id = UR::Object::Type->autogenerate_new_object_id_uuid(); my $ds_type = delete $ds_data->{'is'} || __PACKAGE__; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id ); return $ds; } sub _things_in_list_are_numeric { my $self = shift; foreach ( @{$_[0]} ) { return 0 if (! Scalar::Util::looks_like_number($_)); } return 1; } # Construct a closure to perform an operator test against the given value # The closures return 0 is the test is successful, -1 if unsuccessful but # the file's value was less than $value, and 1 if unsuccessful and greater. # The iterator that churns through the file knows that if it's comparing an # ID/sorted column, and the comparator returns 1 then we've gone past the # point where we can expect to ever find another successful match and we # should stop looking my $ALWAYS_FALSE = sub { -1 }; sub _comparator_for_operator_and_property { my($self,$property,$operator,$value) = @_; no warnings 'uninitialized'; # we're handling ''/undef/null specially below where it matters if ($operator eq 'between') { if ($value->[0] eq '' or $value->[1] eq '') { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { if ($value->[0] > $value->[1]) { # Will never be true Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # numeric 'between' comparison return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} < $value->[0]) { return -1; } elsif (${$_[0]} > $value->[1]) { return 1; } else { return 0; } }; } else { if ($value->[0] gt $value->[1]) { Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # A string 'between' comparison return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} lt $value->[0]) { return -1; } elsif (${$_[0]} gt $value->[1]) { return 1; } else { return 0; } }; } } elsif ($operator eq 'in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { # Numeric 'in' comparison returns undef if we're within the range of the list # but don't actually match any of the items in the list @$value = sort { $a <=> $b } @$value; # sort the values first return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} < $value->[0]) { return -1; } elsif (${$_[0]} > $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if ${$_[0]} == $_; } return -1; } }; } else { # A string 'in' comparison @$value = sort { $a cmp $b } @$value; return sub { if (${$_[0]} lt $value->[0]) { return -1; } elsif (${$_[0]} gt $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if ${$_[0]} eq $_; } return -1; } }; } } elsif ($operator eq 'not in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { return sub { return -1 if (${$_[0]} eq ''); foreach ( @$value ) { return -1 if ${$_[0]} == $_; } return 0; } } else { return sub { foreach ( @$value ) { return -1 if ${$_[0]} eq $_; } return 0; } } } elsif ($operator eq 'like') { # 'like' is always a string comparison. In addition, we can't know if we're ahead # or behind in the file's ID columns, so the only two return values are 0 and 1 return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false # Convert SQL-type wildcards to Perl-type wildcards # Convert a % to a *, and _ to ., unless they're preceeded by \ to escape them. # Not that this isn't precisely correct, as \\% should really mean a literal \ # followed by a wildcard, but we can't be correct in all cases without including # a real parser. This will catch most cases. $value =~ s/(?is_numeric and $self->_things_in_list_are_numeric([$value])) { # Basic numeric comparisons if ($operator eq '=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number return ${$_[0]} <=> $value; }; } elsif ($operator eq '<') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} < $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} <= $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} > $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} >= $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { ${$_[0]} ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { ${$_[0]} ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { return 0 if (${$_[0]} eq ''); # null always != a number ${$_[0]} != $value ? 0 : -1; } } } else { # Basic string comparisons if ($operator eq '=') { return sub { return -1 if (${$_[0]} eq '' xor $value eq ''); return ${$_[0]} cmp $value; }; } elsif ($operator eq '<') { return sub { ${$_[0]} lt $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if (${$_[0]} eq '' or $value eq ''); ${$_[0]} le $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { ${$_[0]} gt $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if (${$_[0]} eq '' or $value eq ''); ${$_[0]} ge $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { ${$_[0]} ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { ${$_[0]} ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { ${$_[0]} ne $value ? 0 : -1; } } } } sub _properties_from_path_spec { my($self) = @_; unless (exists $self->{'__properties_from_path_spec'}) { my $path = $self->path; $path = $path->[0] if ref($path); my @property_names; while($path =~ m/\G\$\{?(\w+)\}?/) { push @property_names, $1; } $self->{'__properties_from_path_spec'} = \@property_names; } return @{ $self->{'__properties_from_path_spec'} }; } sub _generate_loading_templates_arrayref { my($self, $old_sql_cols) = @_; # Each elt in @$column_data is a quad: # [ $class_meta, $property_meta, $table_name, $object_num ] # Keep only the properties with columns (mostly just to remove UR::Object::id my @sql_cols = grep { $_->[1]->column_name } @$old_sql_cols; my $template_data = $self->SUPER::_generate_loading_templates_arrayref(\@sql_cols); return $template_data; } sub _resolve_column_names_from_pathname { my($self,$pathname,$fh) = @_; unless (exists($self->{'__column_names_from_pathname'}->{$pathname})) { if (my $column_names_in_order = $self->columns) { $self->{'__column_names_from_pathname'}->{$pathname} = $column_names_in_order; } else { my $record_separator = $self->record_separator(); my $line = $fh->getline(); $line =~ s/$record_separator$//; # chomp, but for any value # FIXME - to support record-oriented files, we need some replacement for this... my $split_regex = $self->_regex(); my @headers = split($split_regex, $line); $self->{'__column_names_from_pathname'}->{$pathname} = \@headers; } } return $self->{'__column_names_from_pathname'}->{$pathname}; } sub file_is_sorted_as_requested { my($self, $query_plan) = @_; my $sorted_columns = $self->sorted_columns || []; my $order_by_columns = $query_plan->order_by_columns(); for (my $i = 0; $i < @$order_by_columns; $i++) { next if ($order_by_columns->[$i] eq '$.'); # input line number is always sorted next if ($order_by_columns->[$i] eq '__FILE__'); return 0 if $i > $#$sorted_columns; if ($sorted_columns->[$i] ne $order_by_columns->[$i]) { return 0; } } return 1; } # FIXME - this is a copy of parts of _generate_class_data_for_loading from UR::DS::RDBMS sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $parent_class_data = $self->SUPER::_generate_class_data_for_loading($class_meta); my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my $order_by_columns; do { my @id_column_names; for my $inheritance_class_name (@class_hierarchy) { my $inheritance_class_object = UR::Object::Type->get($inheritance_class_name); unless ($inheritance_class_object->table_name) { next; } @id_column_names = #map { # my $t = $inheritance_class_object->table_name; # ($t) = ($t =~ /(\S+)\s*$/); # $t . '.' . $_ #} grep { defined } map { my $p = $inheritance_class_object->property_meta_for_name($_); die ("No property $_ found for " . $inheritance_class_object->class_name . "?") unless $p; $p->column_name; } map { $_->property_name } grep { $_->column_name } $inheritance_class_object->direct_id_property_metas; last if (@id_column_names); } $order_by_columns = \@id_column_names; }; my(@all_table_properties, @direct_table_properties, $first_table_name, $subclassify_by); for my $co ( $class_meta, @{ $parent_class_data->{parent_class_objects} } ) { my $table_name = $co->table_name; next unless $table_name; $first_table_name ||= $co->table_name; # $sub_classification_method_name ||= $co->sub_classification_method_name; # $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_table_properties, map { [$co, $_, $table_name, 0 ] } sort $sort_sub grep { defined $_->column_name && $_->column_name ne '' } UR::Object::Property->get( class_name => $co->class_name ); @direct_table_properties = @all_table_properties if $class_meta eq $co; } my $class_data = { %$parent_class_data, order_by_columns => $order_by_columns, direct_table_properties => \@direct_table_properties, all_table_properties => \@all_table_properties, }; return $class_data; } # Needed for the QueryPlan's processing of order-by params # Params are a list of the 4-tuples [class-meta, prop-meta, table-name, object-num] sub _select_clause_columns_for_table_property_data { my $self = shift; return [ map { $_->[1]->column_name } @_ ]; } # Used to populate the %value_extractor_for_column_name hash # It should return a sub that, when given a row of data from the source, # returns the proper data from that row. # # It's expected to return a sub that accepts ($self, $row, $fh, $filename) # and return a reference to the right data. In most cases, it'll just pluck # out the $column_idx'th element from $@row, but we're using it # to attach special meaning to the $. token sub _create_value_extractor_for_column_name { my($self, $rule, $column_name, $column_idx) = @_; if ($column_name eq '$.') { return sub { my($self, $row, $fh, $filename) = @_; my $line_no = $fh->input_line_number(); return \$line_no; }; } elsif ($column_name eq '__FILE__') { return sub { my($self,$row,$fh,$filename) = @_; return \$filename; }; } else { return sub { my($self, $row, $fh, $filename) = @_; return \$row->[$column_idx]; }; } } sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $class_name = $rule->subject_class_name; my $class_meta = $class_name->__meta__; my $rule_template = $rule->template; # We're defering to the class metadata here because we don't yet know the # pathnames of the files we'll be reading from. If the columns_from_header flag # is set, then there's no way of knowing what the columns are until then my @column_names = grep { defined } map { $class_meta->column_for_property($_) } $class_meta->all_property_names; # FIXME - leaning on the sorted_columns property here means: # 1) It's useless when used where the path spec is a directory and # classes have table_names, since each file is likely to have different # columns # 2) If we ultimately end up reading from more than one file, all the files # must be sorted in the same way. It's possible the user has sorted each # file differently, though in practice it would make for a lot of trouble my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $col = $_; substr($col, 0, 1, ''); $column_is_sorted_descending{$col} = 1; $col; } else { $_; } } @{ $self->sorted_columns || [] }; my %sorted_column_names = map { $_ => 1 } @sorted_column_names; my @unsorted_column_names = grep { ! exists $sorted_column_names{$_} } @column_names; my @rule_column_names_in_order; # The order we should perform rule matches on - value is the name of the column in the file my @comparison_for_column; # closures to call to perform the match - same order as @rule_column_names_in_order my %rule_column_name_to_comparison_index; my(%property_for_column, %operator_for_column, %value_for_column); # These are used for logging my $resolve_comparator_for_column_name = sub { my $column_name = shift; my $property_name = $class_meta->property_for_column($column_name); return unless $rule->specifies_value_for($property_name); my $operator = $rule->operator_for($property_name) || '='; my $rule_value = $rule->value_for($property_name); $property_for_column{$column_name} = $property_name; $operator_for_column{$column_name} = $operator; $value_for_column{$column_name} = $rule_value; my $comp_function = $self->_comparator_for_operator_and_property( $class_meta->property($property_name), $operator, $rule_value); push @rule_column_names_in_order, $column_name; push @comparison_for_column, $comp_function; $rule_column_name_to_comparison_index{$column_name} = $#comparison_for_column; return 1; }; my $sorted_columns_in_rule_count; # How many columns we can consider when trying "the shortcut" for sorted data my %column_is_used_in_sorted_capacity; foreach my $column_name ( @sorted_column_names ) { if (! $resolve_comparator_for_column_name->($column_name) and ! defined($sorted_columns_in_rule_count) ) { # The first time we don't match a sorted column, record the index $sorted_columns_in_rule_count = scalar(@rule_column_names_in_order); } else { $column_is_used_in_sorted_capacity{$column_name} = ' (sorted)'; } } $sorted_columns_in_rule_count ||= scalar(@rule_column_names_in_order); foreach my $column_name ( @unsorted_column_names ) { $resolve_comparator_for_column_name->($column_name); } # sort them by filename my @possible_file_info_list = sort { $a->[0] cmp $b->[0] } $self->resolve_file_info_for_rule_and_path_spec($rule); my $table_name = $class_meta->table_name; if (defined($table_name) and $table_name ne '__default__') { # Tack the final file name onto the end if the class has a table name @possible_file_info_list = map { [ $_->[0] . "/$table_name", $_->[1] ] } @possible_file_info_list; } my $handle_class = $self->handle_class; my $use_quick_read = $handle_class eq 'IO::Handle'; my $split_regex = $self->_regex(); my $logger = $self->_logger('UR_DBI_MONITOR_SQL'); my $record_separator = $self->record_separator; my $monitor_start_time = Time::HiRes::time(); { no warnings 'uninitialized'; $logger->("\nFILE: starting query covering " . scalar(@possible_file_info_list)." files:\n\t" . join("\n\t", map { $_->[0] } @possible_file_info_list ) . "\nFILTERS: " . (scalar(@rule_column_names_in_order) ? join("\n\t", map { $_ . $column_is_used_in_sorted_capacity{$_} . " $operator_for_column{$_} " . (ref($value_for_column{$_}) eq 'ARRAY' ? '[' . join(',',@{$value_for_column{$_}}) .']' : $value_for_column{$_} ) } @rule_column_names_in_order) : '*none*') . "\n\n" ); } my $query_plan = $self->_resolve_query_plan($rule_template); if (@{ $query_plan->{'loading_templates'} } > 1) { Carp::croak(__PACKAGE__ . " does not support joins. The rule was $rule"); } my $loading_template = $query_plan->{loading_templates}->[0]; my @property_names_in_loading_template_order = @{ $loading_template->{'property_names'} }; my @column_names_in_loading_template_order = map { $class_meta->column_for_property($_) } @property_names_in_loading_template_order; my %property_name_to_resultset_index_map; my %column_name_to_resultset_index_map; for (my $i = 0; $i < @property_names_in_loading_template_order; $i++) { my $property_name = $property_names_in_loading_template_order[$i]; $property_name_to_resultset_index_map{$property_name} = $i; $column_name_to_resultset_index_map{$class_meta->column_for_property($property_name)} = $i; } my @iterator_for_each_file; foreach ( @possible_file_info_list ) { my $pathname = $_->[0]; my $property_values_from_path_spec = $_->[1]; my @properties_from_path_spec = keys %$property_values_from_path_spec; my @values_from_path_spec = values %$property_values_from_path_spec; my $pid = $$; # For tracking whether there's been a fork() my $fh = $handle_class->new($pathname); unless ($fh) { $logger->("FILE: Skipping $pathname because it did not open: $!\n"); next; # missing or unopenable files is not fatal } my $column_names_in_order = $self->_resolve_column_names_from_pathname($pathname,$fh); # %value_for_column_name holds subs that return the value for that column. For values # determined from the path resolver, save that value here. Most other values get plucked out # of the line read from the file. The remaining values are special tokens like $. and __FILE__. # These subs are used both for testing whether values read from the data source pass the rule # and for constructing the resultset passed up to the Context my %value_for_column_name; my %column_name_to_index_map; my $ordered_column_names_count = scalar(@$column_names_in_order); for (my $i = 0; $i < $ordered_column_names_count; $i++) { my $column_name = $column_names_in_order->[$i]; next unless (defined $column_name); $column_name_to_index_map{$column_name} = $i; $value_for_column_name{$column_name} = $self->_create_value_extractor_for_column_name($rule, $column_name, $i); } foreach ( '$.', '__FILE__' ) { $value_for_column_name{$_} = $self->_create_value_extractor_for_column_name($rule, $_, undef); $column_name_to_index_map{$_} = undef; } while (my($prop, $value) = each %$property_values_from_path_spec) { my $column = $class_meta->column_for_property($prop); $value_for_column_name{$column} = sub { return \$value }; $column_name_to_index_map{$column} = undef; } # Convert the column_name keys here to indexes into the comparison list my %column_for_this_comparison_is_sorted_descending = map { $rule_column_name_to_comparison_index{$_} => $column_is_sorted_descending{$_} } grep { exists $rule_column_name_to_comparison_index{$_} } keys %column_is_sorted_descending; # rule properties that aren't actually columns in the file should be # satisfied by the path resolution already, so we can strip them out of the # list of columns to test my @rule_columns_in_order = map { $column_name_to_index_map{$_} } grep { exists $column_name_to_index_map{$_} } @rule_column_names_in_order; # And also strip out any items in @comparison_for_column for non-column data my @comparison_for_column_this_file = map { $comparison_for_column[ $rule_column_name_to_comparison_index{$_} ] } grep { exists $column_name_to_index_map{$_} } @rule_column_names_in_order; # Burn through the requsite number of header lines my $lines_read = $fh->input_line_number; my $throwaway_line_count = $self->header_lines; while($throwaway_line_count > $lines_read) { $lines_read++; scalar($fh->getline()); } my $lines_matched = 0; my $log_first_fetch; $log_first_fetch = sub { $logger->(sprintf("FILE: $pathname FIRST FETCH TIME: %.4f s\n\n", Time::HiRes::time() - $monitor_start_time)); $log_first_fetch = \&UR::Util::null_sub; }; my $log_first_match; $log_first_match = sub { $logger->("FILE: $pathname First match after reading $lines_read lines\n\n"); $log_first_match = \&UR::Util::null_sub; }; my $next_record; # This sub reads the next record (line) from the file, splits the line into # columns and puts the data into @$next_record my $record_separator_re = qr($record_separator$); my $read_record_from_file = sub { # Make sure some wise guy hasn't changed this out from under us local $/ = $record_separator; if ($pid != $$) { # There's been a fork() between the original opening and now # This filehandle is no longer valid to read from, but tell() # should still report the right position my $pos = $fh->tell(); $logger->("FILE: reopening file $pathname and seeking to position $pos after fork()\n"); my $fh = $handle_class->new($pathname); unless ($fh) { $logger->("FILE: Reopening $pathname after fork() failed: $!\n"); return; # behave if we're at EOF } $fh->seek($pos, 0); # fast-forward to the old position $pid = $$; } my $line; READ_LINE_FROM_FILE: while(! defined($line)) { # Hack for OSX 10.5. # At EOF, the getline below will return undef. Most builds of Perl # will also set $! to 0 at EOF so you can distinguish between the cases # of EOF (which may have actually happened a while ago because of buffering) # and an actual read error. OSX 10.5's Perl does not, and so $! # retains whatever value it had after the last failed syscall, likely # a stat() while looking for a Perl module. This should have no effect # other platforms where you can't trust $! at arbitrary points in time # anyway $! = 0; $line = $use_quick_read ? <$fh> : $fh->getline(); if ($line and $line !~ $record_separator_re) { # Was a short read - probably at EOF # If the record_separator is a multi-char string, and the last # characters of $line are the first characters of the # record_separator, it's likely (though not certain) that the right # Thing to do is to remove the partial record separator. for (my $keep_chars = length($record_separator); $keep_chars > 0; $keep_chars--) { my $match_rs = substr($record_separator, 0, $keep_chars); if ($line =~ m/$match_rs$/) { substr($line, 0 - $keep_chars) = ''; last; } } } unless (defined $line) { if ($! && ! $fh->eof()) { redo READ_LINE_FROM_FILE if ($! == EAGAIN or $! == EINTR); Carp::croak("read failed for file $pathname: $!"); } # at EOF. Close up shop and remove this fh from the list #flock($fh,LOCK_UN); $fh = undef; $next_record = undef; $logger->("FILE: $pathname at EOF\n" . "FILE: $lines_read lines read for this request. $lines_matched matches in this file\n" . sprintf("FILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n\n", Time::HiRes::time() - $monitor_start_time) ); return; } } $lines_read++; $line =~ s/$record_separator$//; # chomp, but for any value # FIXME - to support record-oriented files, we need some replacement for this... $next_record = [ split($split_regex, $line, $ordered_column_names_count) ]; }; my $number_of_comparisons = @comparison_for_column_this_file; # The file filter iterator. # This sub looks at @$next_record and applies the comparator functions in order. # If it passes all of them, it constructs a resultset row and passes it up to the # multiplexer iterator my $file_filter_iterator = sub { $log_first_fetch->(); FOR_EACH_LINE: for(1) { $read_record_from_file->(); unless ($next_record) { # Done reading from this file return; } for (my $i = 0; $i < $number_of_comparisons; $i++) { my $comparison = $comparison_for_column_this_file[$i]->( $value_for_column_name{ $rule_column_names_in_order[$i] }->($self, $next_record, $fh, $pathname) ); if ( ( ($column_for_this_comparison_is_sorted_descending{$i} and $comparison < 0) or $comparison > 0) and $i < $sorted_columns_in_rule_count ) { # We've gone past the last thing that could possibly match $logger->("FILE: $pathname $lines_read lines read for this request. $lines_matched matches\n" . sprintf("FILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time)); #flock($fh,LOCK_UN); return; } elsif ($comparison) { # comparison didn't match, read another line from the file redo FOR_EACH_LINE; } # That comparison worked... stay in the for() loop for other comparisons } } # All the comparisons return '0', meaning they passed $log_first_match->(); $lines_matched++; my @resultset = map { ref($_) ? $$_ : $_ } map { ref($value_for_column_name{$_}) ? $value_for_column_name{$_}->($self, $next_record, $fh, $pathname) : $value_for_column_name{$_} # constant value from path spec } @column_names_in_loading_template_order; return \@resultset; }; # Higher layers in the loading logic require rows from the data source to be returned # in ID order. If the file contents is not sorted primarily by ID, then we need to do # the less efficient thing by first reading in all the matching rows in one go, sorting # them by ID, then iterating over the results unless ($self->file_is_sorted_as_requested($query_plan)) { my @resultset_indexes_to_sort = map { $column_name_to_resultset_index_map{$_} } @{ $query_plan->order_by_columns() }; $file_filter_iterator = $self->_create_iterator_for_custom_sorted_columns($file_filter_iterator, $query_plan, \%column_name_to_resultset_index_map); } push @iterator_for_each_file, $file_filter_iterator; } if (! @iterator_for_each_file) { return \&UR::Util::null_sub; # No matching files } elsif (@iterator_for_each_file == 1) { return $iterator_for_each_file[0]; # If there's only 1 file, no need to multiplex } my @next_record_for_each_file; # in the same order as @iterator_for_each_file my %column_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } map { index($_, '-') == 0 ? substr($_, 1) : $_ } @{ $query_plan->order_by_columns }; my @resultset_index_sort_sub = map { &_resolve_sorter_for( is_numeric => $column_is_numeric{$_}, is_descending => $column_is_sorted_descending{$_}, column_index => $property_name_to_resultset_index_map{$_}); } @sorted_column_names; my %resultset_idx_is_sorted_descending = map { $column_name_to_resultset_index_map{$_} => 1 } keys %column_is_sorted_descending; my $resultset_sorter = sub { my($idx_a,$idx_b) = shift; foreach my $sort_sub ( @resultset_index_sort_sub ) { my $cmp = $sort_sub->($next_record_for_each_file[$idx_a], $next_record_for_each_file[$idx_b]); return $cmp if $cmp; # done if they're not equal } return 0; }; # This is the iterator returned to the Context, and knows about all the individual # file filter iterators. It compares the next resultset from each of them and # returns the next resultset to the Context my $multiplex_iterator = sub { return unless @iterator_for_each_file; # if they're all run out my $lowest_slot; for(my $i = 0; $i < @iterator_for_each_file; $i++) { unless(defined $next_record_for_each_file[$i]) { $next_record_for_each_file[$i] = $iterator_for_each_file[$i]->(); unless (defined $next_record_for_each_file[$i]) { # That iterator is exhausted, splice it out splice(@iterator_for_each_file, $i, 1); splice(@next_record_for_each_file, $i, 1); return unless (@iterator_for_each_file); # This can happen here if none of the files have matching data redo; } } unless (defined $lowest_slot) { $lowest_slot = $i; next; } my $cmp = $resultset_sorter->($lowest_slot, $i); if ($cmp > 0) { $lowest_slot = $i; } } my $retval = $next_record_for_each_file[$lowest_slot]; $next_record_for_each_file[$lowest_slot] = undef; return $retval; }; return $multiplex_iterator; } # Constructors for subs to sort appropriately sub _resolve_sorter_for { my %params = @_; my $col_idx = $params{'column_index'}; my $is_descending = (exists($params{'is_descending'}) && $params{'is_descending'}) || (exists($params{'is_ascending'}) && $params{'is_ascending'}); my $is_numeric = (exists($params{'is_numeric'}) && $params{'is_numeric'}) || (exists($params{'is_string'}) && $params{'is_string'}); if ($is_descending) { if ($is_numeric) { return sub($$) { $_[1]->[$col_idx] <=> $_[0]->[$col_idx] }; } else { return sub($$) { $_[1]->[$col_idx] cmp $_[0]->[$col_idx] }; } } else { if ($is_numeric) { return sub($$) { $_[0]->[$col_idx] <=> $_[1]->[$col_idx] }; } else { return sub($$) { $_[0]->[$col_idx] cmp $_[1]->[$col_idx] }; } } } # Higher layers in the loading logic require rows from the data source to be returned # in ID order. If the file contents is not sorted primarily by ID, then we need to do # the less efficient thing by first reading in all the matching rows in one go, sorting # them by ID, then iterating over the results sub _create_iterator_for_custom_sorted_columns { my($self, $iterator_this_file, $query_plan, $column_name_to_resultset_index_map) = @_; my @matching; while (my $row = $iterator_this_file->()) { push @matching, $row; # save matches as [id, rowref] } unless (@matching) { return \&UR::Util::null_sub; # Easy, no matches } my $class_meta = $query_plan->class_name->__meta__; my %column_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } map { index($_, '-') == 0 ? substr($_,1) : $_ } @{ $query_plan->order_by_columns }; my @sorters; { no warnings 'numeric'; no warnings 'uninitialized'; @sorters = map { &_resolve_sorter_for(%$_) } map { my $col_name = $_; my $descending = 0; if (index($col_name, '-') == 0) { $descending = 1; substr($col_name, 0, 1, ''); # remove the - } my $col_idx = $column_name_to_resultset_index_map->{$col_name}; { column_index => $col_idx, is_descending => $descending, is_numeric => $column_is_numeric{$col_name} }; } @{ $query_plan->order_by_columns }; } my $sort_by_order_by_columns; if (@sorters == 1) { $sort_by_order_by_columns = $sorters[0]; } else { $sort_by_order_by_columns = sub($$) { foreach (@sorters) { if (my $rv = $_->(@_)) { return $rv; } } return 0; }; } @matching = sort $sort_by_order_by_columns @matching; return sub { return shift @matching; }; } sub initializer_should_create_column_name_for_class_properties { 1; } # The string used to join fields of a row together when writing # # Since the 'delimiter' property is interpreted as a regex in the reading # code, we'll try to be smart about making a real string from that. # # subclasses can override this to provide a different implementation sub column_join_string { my $self = shift; my $join_pattern = $self->delimiter; # make some common substitutions... if ($join_pattern eq '\s*,\s*') { # The default... return ', '; } $join_pattern =~ s/\\s*//g; # Turn 0-or-more whitespaces to nothing $join_pattern =~ s/\\t/\t/; # tab $join_pattern =~ s/\\s/ /; # whitespace return $join_pattern; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { Carp::croak("Cannot call _sync_database as a class method on a non-singleton class"); } } $DB::single=1; my $changed_objects = delete $params{'changed_objects'}; my $path_spec = $self->path; # First, bin up the changed objects by their class' table_name my %objects_for_path; foreach my $obj ( @$changed_objects ) { my @path = $self->resolve_file_info_for_rule_and_path_spec($obj, $path_spec); if (!@path) { $self->error_message("Couldn't resolve destination file for object " .$obj->class." ID ".$obj->id.": ".Data::Dumper::Dumper($obj)); return; } elsif (@path > 1) { $self->error_message("Got multiple filenames when resolving destination file for object " . $obj->class." ID ".$obj->id.": ".join(', ', @path)); } $objects_for_path{ $path[0]->[0] } ||= []; push @{ $objects_for_path{ $path[0]->[0] } }, $obj; } my %objects_for_pathname; foreach my $path ( keys %objects_for_path ) { foreach my $obj ( @{ $objects_for_path{$path} } ) { my $class_meta = $obj->__meta__; my $table_name = $class_meta->table_name; my $pathname = $path; if (defined($table_name) and $table_name ne '__default__') { $pathname .= '/' . $table_name; } $objects_for_pathname{$pathname} ||= []; push @{ $objects_for_pathname{$pathname} }, $obj; } } my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $s = $_; substr($s, 0, 1, ''); $column_is_sorted_descending{$s} = $s; } else { $_; } } @{ $self->sorted_columns() || [] }; my $handle_class = $self->handle_class; my $use_quick_read = $handle_class->isa('IO::Handle'); my $join_string = $self->column_join_string; my $record_separator = $self->record_separator; my $split_regex = $self->_regex(); local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $logger = $self->_logger('UR_DBI_MONITOR_SQL'); my $total_save_time = Time::HiRes::time(); $logger->("FILE: Saving changes to ".scalar(keys %objects_for_pathname) . " files:\n\t" . join("\n\t", keys(%objects_for_pathname)) . "\n\n"); foreach my $pathname ( keys %objects_for_pathname ) { my $use_quick_rename; my $containing_directory = File::Basename::dirname($pathname); unless (-d $containing_directory) { File::Path::mkpath($containing_directory); } if (-w $containing_directory) { $use_quick_rename = 1; } elsif (! -w $pathname) { Carp::croak("Cannot save to file $pathname: Neither the directory nor the file are writable"); } my $read_fh = $handle_class->new($pathname); # Objects going to the same file should all be of a common class my $class_meta = $objects_for_pathname{$pathname}->[0]->__meta__; my @property_names_that_are_sorted = map { $class_meta->property_for_column($_) } @sorted_column_names; # Returns true of the passed-in object has a change in one of the sorted columns my $object_has_changed_sorted_column = sub { my $obj = shift; foreach my $prop ( @property_names_that_are_sorted ) { if (UR::Context->_get_committed_property_value($obj, $prop) ne $obj->$prop) { return 1; } } return 0; }; my $column_names_in_file = $self->_resolve_column_names_from_pathname($pathname, $read_fh); my $column_names_count = @$column_names_in_file; my %column_name_to_index; for (my $i = 0; $i < @$column_names_in_file; $i++) { $column_name_to_index{$column_names_in_file->[$i]} = $i; } # This lets us take a hash slice of the object and get a row for the file my @property_names_in_column_order = map { $class_meta->property_for_column($_) } @$column_names_in_file; my %column_name_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } @$column_names_in_file; my $insert = []; my $update = {}; my $delete = {}; foreach my $obj ( @{ $objects_for_pathname{$pathname} } ) { if ($obj->isa('UR::Object::Ghost')) { # This should be removed from the file my $original = $obj->{'db_committed'}; my $line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } elsif ($obj->{'db_committed'}) { # this is a changed object my $original = $obj->{'db_committed'}; if ($object_has_changed_sorted_column->($obj)) { # One of hte sorted columns has changed. Model this as a delete and insert push @$insert, [ @{$obj}{@property_names_in_column_order} ]; my $line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } else { # This object is changed since it was read in the file my $original_line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; my $changed_line = join($join_string, @{$obj}{@property_names_in_column_order}) . $record_separator; $update->{$original_line} = $changed_line; } } else { # This object is new and should be added to the file push @$insert, [ @{$obj}{@property_names_in_column_order} ]; } } my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $s = $_; substr($s, 0, 1, ''); $column_is_sorted_descending{$s} = $s; } else { $_; } } @{ $self->sorted_columns() || [] }; my $row_sort_sub; if (@sorted_column_names) { my @comparison_subs = map { &_resolve_sorter_for(is_numeric => $column_name_is_numeric{$_}, is_descending => $column_is_sorted_descending{$_}, column_index => $column_name_to_index{$_}) } @sorted_column_names; $row_sort_sub = sub ($$) { foreach my $comparator ( @comparison_subs ) { my $cmp = $comparator->($_[0], $_[1]); return $cmp if $cmp; } return 0; }; # Put the rows-to-insert in sorted order my @insert_sorted = sort $row_sort_sub @$insert; $insert = \@insert_sorted; } my $write_fh = $use_quick_rename ? File::Temp->new(DIR => $containing_directory) : File::Temp->new(); unless ($write_fh) { Carp::croak("Can't save changes for $pathname: Can't create temporary file for writing: $!"); } my $monitor_start_rime = Time::HiRes::time(); my $time = time(); $logger->(sprintf("\nFILE: SYNC DATABASE AT %s [%s]. Started transaction for %s to temp file %s\n", $time, scalar(localtime($time)), $pathname, $write_fh->filename)); # Write headers to the new file for (my $i = 0; $i < $self->header_lines; $i++) { my $line = $use_quick_read ? <$read_fh> : $read_fh->getline(); $write_fh->print($line); } my $line; READ_A_LINE: while(1) { unless ($line) { $line = $use_quick_read ? <$read_fh> : $read_fh->getline(); last unless defined $line; } if (@sorted_column_names and scalar(@$insert)) { # There are sorted things waiting to insert my $chomped = $line; $chomped =~ s/$record_separator$//; # chomp, but for any value my $row = [ split($split_regex, $chomped, $column_names_count) ]; my $cmp = $row_sort_sub->($row, $insert->[0]); if ($cmp > 0) { # write the object's data no warnings 'uninitialized'; # Some of the object's data may be undef my $new_row = shift @$insert; my $new_line = join($join_string, @$new_row) . $record_separator; $logger->("FILE: INSERT >>$new_line<<\n"); $write_fh->print($new_line); # Don't undef the last line read, meaning it could still be written to the output... next READ_A_LINE; } } if (my $obj = delete $delete->{$line}) { $logger->("FILE: DELETE >>$line<<\n"); } elsif (my $changed = delete $update->{$line}) { $logger->("FILE: UPDFATE replace >>$line<< with >>$changed<<\n"); $write_fh->print($changed); } else { # This line form the file was unchanged in the app $write_fh->print($line); } $line = undef; } if (keys %$delete) { $self->warning_message("There were " . scalar( keys %$delete) . " deleted " . $class_meta->class_name . " objects that did not match data in file $pathname"); } if (keys %$update) { $self->warning_message("There were " . scalar( keys %$delete) . " updated " . $class_meta->class_name . " objects that did not match data in file $pathname"); } # finish out by writing the rest of the new data foreach my $new_row ( @$insert ) { no warnings 'uninitialized'; # Some of the object's data may be undef my $new_line = join($join_string, @$new_row) . $record_separator; $logger->("FILE: INSERT >>$new_line<<\n"); $write_fh->print($new_line); } my $changed_objects = $objects_for_pathname{$pathname}; unless ($self->_set_specified_objects_saved_uncommitted( $changed_objects )) { Carp::croak("Error setting objects to a saved state after syncing"); } # These closures will keep $write_fh in scope and delay their removal until # commit() or rollback(). Call these with no args to commit, and one arg (doesn't # matter what) to roll back my $commit = $use_quick_rename ? sub { if (@_) { $self->_set_specified_objects_saved_rolled_back($changed_objects); } else { my $temp_filename = $write_fh->filename; $logger->("FILE: COMMIT rename $temp_filename => $pathname\n"); unless (rename($temp_filename, $pathname)) { $self->error_message("Can't rename $temp_filename to $pathname: $!"); return; } $self->_set_specified_objects_saved_committed($changed_objects); } return 1; } : sub { if (@_) { $self->_set_specified_objects_saved_rolled_back($changed_objects); } else { my $temp_filename = $write_fh->filename; $logger->("FILE: COMMIT copy " . $temp_filename . " => $pathname\n"); my $read_fh = IO::File->new($temp_filename); unless ($read_fh) { $self->error_message("Can't open file $temp_filename for reading: $!"); return; } my $copy_fh = IO::File->new($pathname, 'w'); unless ($copy_fh) { $self->error_message("Can't open file $pathname for writing: $!"); return; } while(<$read_fh>) { $copy_fh->print($_); } $copy_fh->close(); $read_fh->close(); $self->_set_specified_objects_saved_committed($changed_objects); } return 1; }; $write_fh->close(); $self->{'__saved_uncommitted'} ||= []; push @{ $self->{'__saved_uncommitted'} }, $commit; $time = time(); $logger->("\nFILE: SYNC DATABASE finished ".$write_fh->filename . "\n"); } $logger->(sprintf("Saved changes to %d files in %.4f s\n", scalar(@{ $self->{'__saved_uncommitted'}}), Time::HiRes::time() - $total_save_time)); return 1; } sub commit { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } if ($self->{'__saved_uncommitted'}) { foreach my $commit ( @{ $self->{'__saved_uncommitted'}}) { $commit->(); } } delete $self->{'__saved_uncommitted'}; return 1; } sub rollback { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } if ($self->{'__saved_uncommitted'}) { foreach my $commit ( @{ $self->{'__saved_uncommitted'}}) { $commit->('rollback'); } } delete $self->{'__saved_uncommitted'}; return 1; } 1; __END__ =pod =head1 NAME UR::DataSource::Filesystem - Get and save objects to delimited text files =head1 SYNOPSIS # Create an object for the data file my $people_data = UR::DataSource::Filesystem->create( columns => ['person_id','name','age','street_address'], sorted_columns => ['age','person_id'], path => '/var/lib/people/$state/$city/people.txt', delimiter => "\t", # between columns in the file record_separator => "\n", # between lines in the file ); # Define an entity class for the people in the file class MyProgram::Person { id_by => 'person_id', has => [ name => { is => 'String' }, age => { is => 'Number' }, street_address => { is => 'String' }, city => { is => 'String' }, state => { is => 'String' }, ], data_source_id => $people_data->id, }; # Get all people that live in any city named Springfield older than 40 my @springfielders = MyProgram::Person->get(city => 'Springfield', 'age >' => 40); =head1 DESCRIPTION A Filesystem data source object represents one or more files on the fileystem. In the simplest case, the object's 'path' property names a file that stores the data. =head2 Properties These properties determine the configuration for the data source. =over 4 =item path path is a string representing the path to the files. Besides just being a simple pathname to one file, the string can also be a specification of many similar files, or a directory containing multiple files. See below for more information about 'path' =item record_separator The separator between lines in the file. This gets stored in $/ before calling getline() to read data. The default record_separator is "\n". =item delimiter The separator between columns in the file. It is used to construct a regex with qr() to split() a line into a list of values. The default delimiter is '\s*,\s*', meaning that the file is separated by commas. Another common value would be "\t" for tabs. =item columns A listref of column names in the file. Just as SQL tables have columns, Filesystem files also have named columns so the system knows how to read the file data into object properties. A Filesystem data source does not need to specify named columns if the 'columns_from_header' property is true. Classes that use the Filesystem data source attach their properties to the data source's columns via the 'column_name' metadata. Besides the columns directly named in the 'columns' list, two additional column-like tokens may be used as a column_name: '__FILE__' and '$.'. __FILE__ means the object's property will hold the name of the file the data was read from. $. means the value will be the input line number from the file. These are useful when iterating over the contents of a file. Since these two fake columns are always considered "sorted", it makes reading from the file faster in some cases. See the 'sorted_columns' discussion below for more information. =item sorted_columns A listref of column names that the file is sorted by, in the order of the sorting. If a column is sorted in descending order, put a minus (-) in front of the name. If the file is sorted by multiple columns, say first by last_name and then by first_name, then include them both: sorted_columns => ['last_name','first_name'] The system uses this information to know when to stop reading if a query is done on a sorted column. It's also used to determine whether a query done on the data source matches the sort order of the file. If not, then the data must be gathered in two passes. The first pass finds records in the file that match the filter. After that, the matching records are sorted in the same way the query is requesting before returning the data to the Context. The Context expects incoming data to always be sorted by at least the class' ID properties. If the file is unsorted and the caller wants to be able to iterate over the data, then it is common to have the class' ID properties specified like this: id_by => [ file => { is => 'String', column_name => '__FILE__' }, line => { is => 'Integer', column_name => '$.' }, ] Otherwise, it will need to read in the whole file and sort the contents before returning the first row of data from its iterator. =item columns_from_header If true, the system will read the first line of the file to determine what the column names are. =item header_lines The number of lines at the top of the file that do not contain entity data. When the file is opened, this number of lines are skipped before reading data. If the columns_from_header flag is true, the header_lines value should be at least 1. =item handle_class Which class to use for reading and writing to the file. The default is IO::File. Any other value must refer to a class that has the same interface as IO::File, in particular: new, input_line_number, getline, tell, seek and print. =back =head2 Path specification Besides refering to just one file on the filesystem, the path spec is a recipe for finding files in a directory tree. If a class using a Filesystem data source does not have 'table_name' metadata, then the path specification must resolve to file names. Alternatively, classes may specify their 'table_name' which is interpreted as a file within the directory indicated by the path specification. Three kinds of special tokens can also appear in a file spec: =over 4 =item $property When querying, the system will extract the value (or values, for an in-clause) of $property from the BoolExpr when constructing the pathname. If the BoolExpr does not have a value for that property, then the system will do a shell glob to find the possible values. For example, given this path spec and query: path => '/var/people/$state/$city/people.txt' my @people = MyProgram::People->get(city => 'Springfield', 'age >' => 40); it would find the data files using the glob expression /var/people/*/Springfield/people.txt It also knows that any objects coming from the file /var/people/CA/Springfield/people.txt must have the value 'CA' for their 'state' property, even though that information is not in the contents of the file. When committing changes back to the file, the object property values are used to determine which file it should be saved to. The property name can also be wrapped in braces: /var/people/${state}_US/city_${city}/people.txt =item &method The replacement value is resolved by calling the named method on the subject class of the query. The method is called like this: $replacement = $subject_class->$method( $boolexpr_or_object); During a query, the method is passed a BoolExpr; during a commit, the method is passed an object. It must return a string. The method name can also be wrapped in braces: /&{resolve_prefix}.dir/people.txt =item *, ? Literal shell glob wildcards are honored when finding files, but their values are not used to supply values to objects. =back =head2 Environment Variables If the environment variable $UR_DBI_MONITOR_SQL is true, then the Filesystem data source will print information about the queries it runs. =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource =cut Meta.pm100664023532023421 1370312544604516 16233 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use version; use DBD::SQLite; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Meta', is => ['UR::DataSource::SQLite'], has_constant => [ owner => { value => 'main' }, ], ); sub _resolve_class_name_for_table_name_fixups { my $self = shift->_singleton_object; if ($_[0] =~ m/Dd/) { $_[0] = "DataSource::RDBMS::"; } return @_; } # Do a DB dump at commit time sub dump_on_commit { 1; } # This is the template for the schema: our $METADATA_DB_SQL =<__meta__->module_path(); my $meta_datasource_name = $namespace_name . '::DataSource::Meta'; my $meta_datasource = UR::Object::Type->define( class_name => $meta_datasource_name, is => 'UR::DataSource::Meta', is_abstract => 0, ); my $meta_datasource_src = $meta_datasource->resolve_module_header_source(); my $meta_datasource_filename = $meta_datasource->module_base_name(); my $meta_datasource_filepath = $namespace_path; return unless defined($meta_datasource_filepath); # This namespace could be fabricated at runtime $meta_datasource_filepath =~ s/.pm//; $meta_datasource_filepath .= '/DataSource'; mkdir($meta_datasource_filepath); unless (-d $meta_datasource_filepath) { die "Failed to create directory $meta_datasource_filepath: $!"; } $meta_datasource_filepath .= '/Meta.pm'; # Write the Meta DB datasource Module if (-e $meta_datasource_filepath) { Carp::croak("Can't create new MetaDB datasource Module $meta_datasource_filepath: File already exists"); } my $fh = IO::File->new("> $meta_datasource_filepath"); unless ($fh) { Carp::croak("Can't create MetaDB datasource Module $meta_datasource_filepath: $!"); } $fh->printf($module_template, $meta_datasource_name, $meta_datasource_src); # Write the skeleton SQLite file my $meta_db_file = $meta_datasource->class_name->_data_dump_path; IO::File->new(">$meta_db_file")->print($UR::DataSource::Meta::METADATA_DB_SQL); return ($meta_datasource, $meta_db_file); } sub _dbi_connect_args { my $self = shift; my @connection = $self->SUPER::_dbi_connect_args(@_); if(version->parse($DBD::SQLite::VERSION) >= version->parse('1.38_01')) { my $connect_attr = $connection[3] ||= {}; $connect_attr->{sqlite_use_immediate_transaction} = 0; } return @connection; } 1; =pod =head1 NAME UR::DataSource::Meta - Data source for the MetaDB =head1 SYNOPSIS my $meta_table = UR::DataSource::RDBMS::Table->get( table_name => 'DD_TABLE' namespace => 'UR', ); my @myapp_tables = UR::DataSource::RDBMS::Table->get( namespace => 'MyApp', ); =head1 DESCRIPTION UR::DataSource::Meta a datasource that contains all table/column meta data for the UR namespace itself. Essentially the schema schema. =head1 INHERITANCE UR::DataSource::Meta is a subclass of L =head1 get() required parameters C or C are required parameters when calling C on any MetaDB-sourced object types. =cut Meta.sqlite3-dump100664023532023421 3026112544604516 20144 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, table_name) ); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, table_name, bitmap_index_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank','integer',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','er_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_length','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_ddl_time','timestamp',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','nullable','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_type','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','remarks','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','remarks','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name',2); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,table_name,constraint_name,column_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, table_name, fk_constraint_name, column_name) ); COMMIT; Meta.sqlite3-dump-boostrap100664023532023421 3211212544604516 21770 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank','integer',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','er_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_length','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_ddl_time','timestamp',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','nullable','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_type','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','remarks','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','remarks','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name',6); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name',3); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); COMMIT; Meta.sqlite3-schema100664023532023421 434012544604516 20416 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourceCREATE TABLE IF NOT EXISTS dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE IF NOT EXISTS dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE IF NOT EXISTS dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE IF NOT EXISTS dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE IF NOT EXISTS dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE IF NOT EXISTS dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE IF NOT EXISTS dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); MySQL.pm100664023532023421 1436112544604516 16313 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::MySQL; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::MySQL', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "mysql" } #sub server { # my $self = shift->_singleton_object(); # $self->_init_database; # return $self->_database_file_path; #} sub owner { shift->_singleton_object->login } #sub login { # undef #} # #sub auth { # undef #} sub _default_sql_like_escape_string { undef }; # can't do an 'escape' clause with the 'like' operator sub can_savepoint { 1;} *_init_created_dbh = \&init_created_handle; sub init_created_handle { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(pg_|sql_|URMETA)/; } # # for concurrency's sake we need to use dummy tables in place of sequence generators here, too # sub _get_sequence_name_for_table_and_column { my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_SEQ', $table_name, $column_name); #$DB::single = 1; unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTO_INCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } sub get_bitmap_index_details_from_data_dictionary { # Mysql dosen't have bitmap indexes. return []; } sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("savepoint $sp_name"); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("rollback to savepoint $sp_name"); } sub _resolve_order_by_clause_for_column { my($self, $column_name, $query_plan, $property_meta) = @_; my $is_optional = $property_meta->is_optional; my $column_clause = $column_name; # default, usual case if ($is_optional) { if ($query_plan->order_by_column_is_descending($column_name)) { $column_clause = "CASE WHEN $column_name ISNULL THEN 0 ELSE 1 END, $column_name DESC"; } else { $column_clause = "CASE WHEN $column_name ISNULL THEN 1 ELSE 0 END, $column_name"; } } elsif ($query_plan->order_by_column_is_descending($column_name)) { $column_clause = $column_name . ' DESC'; } return $column_clause; } # FIXME This works on Mysql 4.x (and later?). Mysql5 has a database called # IMFORMATION_SCHEMA that may be more useful for these kinds of queries sub get_unique_index_details_from_data_dictionary { my($self, $owner_name, $table_name) = @_; my $dbh = $self->get_default_handle(); return undef unless $dbh; #$table_name = $dbh->quote($table_name); my $sql = qq(SHOW INDEX FROM $table_name FROM $owner_name); my $sth = $dbh->prepare($sql); return undef unless $sth; $sth->execute(); my $ret; while (my $data = $sth->fetchrow_hashref()) { next if ($data->{'Non_unique'}); $ret->{$data->{'Key_name'}} ||= []; push @{ $ret->{ $data->{'Key_name'} } }, $data->{'Column_name'}; } return $ret; } sub get_column_details_from_data_dictionary { my $self = shift; # Mysql seems wierd about the distinction between catalog/database and schema/owner # For 'ur update classes', it works if we just pass in undef for catalog # The passed-in args are: $self,$catalog,$schema,$table,$column my $catalog = shift; return $self->SUPER::get_column_details_from_data_dictionary(undef, @_); } sub get_foreign_key_details_from_data_dictionary { my $self = shift; # Mysql requires undef in some fields instead of an empty string my @new_params = map { length($_) ? $_ : undef } @_; return $self->SUPER::get_foreign_key_details_from_data_dictionary(@new_params); } my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'TINYINT' => ['Integer', undef], 'SMALLINT' => ['Integer', undef], 'MEDIUMINT' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'BINARY' => ['Text', undef], 'VARBINARY' => ['Text', undef], 'TINYTEXT' => ['Text', undef], 'MEDIUMTEXT' => ['Text', undef], 'LONGTEXT' => ['Text', undef], 'TINYBLOB' => ['Blob', undef], 'MEDIUMBLOB' => ['Blob', undef], 'LONGBLOB' => ['Blob', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; $type = $class->normalize_vendor_type($type); my $urtype = $ur_data_type_for_vendor_data_type{$type}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } 1; =pod =head1 NAME UR::DataSource::MySQL - MySQL specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the MySQL-specific methods necessary for interacting with MySQL databases =head1 SEE ALSO L, L =cut Oracle.pm100664023532023421 5304712544604516 16557 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::Oracle; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Oracle', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); sub driver { "Oracle" } sub owner { shift->_singleton_object->login } sub can_savepoint { 1 } # Oracle supports savepoints inside transactions sub does_support_recursive_queries { 'connect by' }; sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("savepoint $sp_name"); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("rollback to $sp_name"); } my $DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'; my $TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SSXFF'; sub _set_date_format { my $self = shift; foreach my $sql ("alter session set NLS_DATE_FORMAT = '$DATE_FORMAT'", "alter session set NLS_TIMESTAMP_FORMAT = '$TIMESTAMP_FORMAT'" ) { $self->do_sql($sql); } } *_init_created_dbh = \&init_created_handle; sub init_created_handle { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; $self->_set_date_format(); return $dbh; } sub _dbi_connect_args { my @args = shift->SUPER::_dbi_connect_args(@_); $args[3]{ora_module_name} = (UR::Context::Process->get_current->prog_name || $0); return @args; } sub _prepare_for_lob { { ora_auto_lob => 0 } } sub _post_process_lob_values { my ($self, $dbh, $lob_id_arrayref) = @_; return map { if (defined($_)) { my $length = $dbh->ora_lob_length($_); my $data = $dbh->ora_lob_read($_, 1, $length); # TODO: bind to a file for items of a certain size to save RAM. # Special work with tying a scalar to the file? $data; } else { undef; } } @$lob_id_arrayref; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /\$/; } sub get_table_last_ddl_times_by_table_name { my $self = shift; my $sql = qq| select object_name table_name, last_ddl_time from all_objects o where o.owner = ? and (o.object_type = 'TABLE' or o.object_type = 'VIEW') |; my $data = $self->get_default_handle->selectall_arrayref( $sql, undef, $self->owner ); return { map { @$_ } @$data }; }; sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; # we may need to change how this db handle is gotten my $dbh = $self->get_default_handle; my $new_id = $dbh->selectrow_array("SELECT " . $sequence_name . ".nextval from DUAL"); if ($dbh->err) { die "Failed to prepare SQL to generate a column id from sequence: $sequence_name.\n" . $dbh->errstr . "\n"; return; } return $new_id; } sub get_bitmap_index_details_from_data_dictionary { my($self, $table_name) = @_; my $sql = qq( select c.table_name,c.column_name,c.index_name from all_indexes i join all_ind_columns c on i.index_name = c.index_name where i.index_type = 'BITMAP' ); my @select_params; if ($table_name) { @select_params = $self->_resolve_owner_and_table_from_table_name($table_name); $sql .= " and i.table_owner = ? and i.table_name = ?"; } my $dbh = $self->get_default_handle; my $rows = $dbh->selectall_arrayref($sql, undef, @select_params); return undef unless $rows; my @ret = map { { table_name => $_->[0], column_name => $_->[1], index_name => $_->[2] } } @$rows; return \@ret; } sub get_unique_index_details_from_data_dictionary { my ($self, $owner_name, $table_name) = @_; my $sql = qq( select cc.constraint_name, cc.column_name from all_cons_columns cc join all_constraints c on c.constraint_name = cc.constraint_name and c.owner = cc.owner and c.constraint_type = 'U' where cc.table_name = ? and cc.owner = ? union select ai.index_name, aic.column_name from all_indexes ai join all_ind_columns aic on aic.index_name = ai.index_name and aic.index_owner = ai.owner where ai.uniqueness = 'UNIQUE' and aic.table_name = ? and aic.index_owner = ? ); my $dbh = $self->get_default_handle(); return undef unless $dbh; my $sth = $dbh->prepare($sql); return undef unless $sth; $sth->execute($table_name, $owner_name, $table_name, $owner_name); my $ret; while (my $data = $sth->fetchrow_hashref()) { $ret->{$data->{'CONSTRAINT_NAME'}} ||= []; push @{ $ret->{ $data->{CONSTRAINT_NAME} } }, $data->{COLUMN_NAME}; } return $ret; } sub set_userenv { # there are two places to set these oracle variables- # 1. this method in UR::DataSource::Oracle is a class method # that can be called to change the values later # 2. the method in YourSubclass::DataSource::Oracle is called in # init_created_handle which is called while the datasource # is still being set up- it operates directly on the db handle my ($self, %p) = @_; my $dbh = $p{'dbh'} || $self->get_default_handle(); # module is application name my $module = $p{'module'} || $0; # storing username in 'action' oracle variable my $action = $p{'action'}; if (! defined($action)) { $action = getpwuid($>); # real UID } my $sql = q{BEGIN dbms_application_info.set_module(?, ?); END;}; my $sth = $dbh->prepare($sql); if (!$sth) { warn "Couldnt prepare query to set module/action in Oracle"; return undef; } $sth->execute($module, $action) || warn "Couldnt set module/action in Oracle"; } sub get_userenv { # there are two ways to set these values but this is # the only way to retrieve the values after they are set my ($self, $dbh) = @_; if (!$dbh) { $dbh = $self->get_default_handle(); } if (!$dbh) { warn "No dbh"; return undef; } my $sql = q{ SELECT sys_context('USERENV','MODULE') as module, sys_context('USERENV','ACTION') as action FROM dual }; my $sth = $dbh->prepare($sql); return undef unless $sth; $sth->execute() || die "execute failed: $!"; my $r = $sth->fetchrow_hashref(); return $r; } my %ur_data_type_for_vendor_data_type = ( 'VARCHAR2' => ['Text', undef], 'BLOB' => ['XmlBlob', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; $type = $class->normalize_vendor_type($type); my $urtype = $ur_data_type_for_vendor_data_type{$type}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; for (my $n = 0; $n < @$column_objects; $n++) { next unless defined ($column_objects->[$n]); # No metaDB info for this one if ($column_objects->[$n]->data_type eq 'BLOB') { $sth->bind_param($n+1, undef, { ora_type => 23 }); } } } sub get_connection_debug_info { my $self = shift; my @debug_info = $self->SUPER::get_connection_debug_info(@_); push @debug_info, ( "DBD::Oracle Version: ", $DBD::Oracle::VERSION, "\n", "TNS_ADMIN: ", $ENV{TNS_ADMIN}, "\n", "ORACLE_HOME: ", $ENV{ORACLE_HOME}, "\n", ); return @debug_info; } # This is a near cut-and-paste from DBD::Oracle, with the exception that # the query hint is removed, since it performs poorly on Oracle 11 sub get_table_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_table_details_from_data_dictionary(@_); } my($CatVal, $SchVal, $TblVal, $TypVal) = @_; my $dbh = $self->get_default_handle(); # XXX add knowledge of temp tables, etc # SQL/CLI (ISO/IEC JTC 1/SC 32 N 0595), 6.63 Tables if (ref $CatVal eq 'HASH') { ($CatVal, $SchVal, $TblVal, $TypVal) = @$CatVal{'TABLE_CAT','TABLE_SCHEM','TABLE_NAME','TABLE_TYPE'}; } my @Where = (); my $SQL; if ( defined $CatVal && $CatVal eq '%' && (!defined $SchVal || $SchVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19a $SQL = <<'SQL'; SELECT NULL TABLE_CAT , NULL TABLE_SCHEM , NULL TABLE_NAME , NULL TABLE_TYPE , NULL REMARKS FROM DUAL SQL } elsif ( defined $SchVal && $SchVal eq '%' && (!defined $CatVal || $CatVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19b $SQL = <<'SQL'; SELECT NULL TABLE_CAT , s TABLE_SCHEM , NULL TABLE_NAME , NULL TABLE_TYPE , NULL REMARKS FROM ( SELECT USERNAME s FROM ALL_USERS UNION SELECT 'PUBLIC' s FROM DUAL ) ORDER BY TABLE_SCHEM SQL } elsif ( defined $TypVal && $TypVal eq '%' && (!defined $CatVal || $CatVal eq '') && (!defined $SchVal || $SchVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19c $SQL = <<'SQL'; SELECT NULL TABLE_CAT , NULL TABLE_SCHEM , NULL TABLE_NAME , t.tt TABLE_TYPE , NULL REMARKS FROM ( SELECT 'TABLE' tt FROM DUAL UNION SELECT 'VIEW' tt FROM DUAL UNION SELECT 'SYNONYM' tt FROM DUAL UNION SELECT 'SEQUENCE' tt FROM DUAL ) t ORDER BY TABLE_TYPE SQL } else { $SQL = <<'SQL'; SELECT * FROM ( SELECT NULL TABLE_CAT , t.OWNER TABLE_SCHEM , t.TABLE_NAME TABLE_NAME , decode(t.OWNER , 'SYS' , 'SYSTEM ' , 'SYSTEM' , 'SYSTEM ' , '' ) || t.TABLE_TYPE TABLE_TYPE , c.COMMENTS REMARKS FROM ALL_TAB_COMMENTS c , ALL_CATALOG t WHERE c.OWNER (+) = t.OWNER AND c.TABLE_NAME (+) = t.TABLE_NAME AND c.TABLE_TYPE (+) = t.TABLE_TYPE ) SQL if ( defined $SchVal ) { push @Where, "TABLE_SCHEM LIKE '$SchVal' ESCAPE '\\'"; } if ( defined $TblVal ) { push @Where, "TABLE_NAME LIKE '$TblVal' ESCAPE '\\'"; } if ( defined $TypVal ) { my $table_type_list; $TypVal =~ s/^\s+//; $TypVal =~ s/\s+$//; my @ttype_list = split (/\s*,\s*/, $TypVal); foreach my $table_type (@ttype_list) { if ($table_type !~ /^'.*'$/) { $table_type = "'" . $table_type . "'"; } $table_type_list = join(", ", @ttype_list); } push @Where, "TABLE_TYPE IN ($table_type_list)"; } $SQL .= ' WHERE ' . join("\n AND ", @Where ) . "\n" if @Where; $SQL .= " ORDER BY TABLE_TYPE, TABLE_SCHEM, TABLE_NAME\n"; } my $sth = $dbh->prepare($SQL) or return undef; $sth->execute or return undef; $sth; } sub get_column_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_column_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my $attr = ( ref $_[0] eq 'HASH') ? $_[0] : { 'TABLE_SCHEM' => $_[1],'TABLE_NAME' => $_[2],'COLUMN_NAME' => $_[3] }; my($typecase,$typecaseend) = ('',''); my $v = DBD::Oracle::db::ora_server_version($dbh); if (!defined($v) or $v->[0] >= 8) { $typecase = <<'SQL'; CASE WHEN tc.DATA_TYPE LIKE 'TIMESTAMP% WITH% TIME ZONE' THEN 95 WHEN tc.DATA_TYPE LIKE 'TIMESTAMP%' THEN 93 WHEN tc.DATA_TYPE LIKE 'INTERVAL DAY% TO SECOND%' THEN 110 WHEN tc.DATA_TYPE LIKE 'INTERVAL YEAR% TO MONTH' THEN 107 ELSE SQL $typecaseend = 'END'; } my $SQL = <<"SQL"; SELECT * FROM ( SELECT to_char( NULL ) TABLE_CAT , tc.OWNER TABLE_SCHEM , tc.TABLE_NAME TABLE_NAME , tc.COLUMN_NAME COLUMN_NAME , $typecase decode( tc.DATA_TYPE , 'MLSLABEL' , -9106 , 'ROWID' , -9104 , 'UROWID' , -9104 , 'BFILE' , -4 -- 31? , 'LONG RAW' , -4 , 'RAW' , -3 , 'LONG' , -1 , 'UNDEFINED', 0 , 'CHAR' , 1 , 'NCHAR' , 1 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 8, 3 ) , 'FLOAT' , 8 , 'VARCHAR2' , 12 , 'NVARCHAR2', 12 , 'BLOB' , 30 , 'CLOB' , 40 , 'NCLOB' , 40 , 'DATE' , 93 , NULL ) $typecaseend DATA_TYPE -- ... , tc.DATA_TYPE TYPE_NAME -- std.? , decode( tc.DATA_TYPE , 'LONG RAW' , 2147483647 , 'LONG' , 2147483647 , 'CLOB' , 2147483647 , 'NCLOB' , 2147483647 , 'BLOB' , 2147483647 , 'BFILE' , 2147483647 , 'NUMBER' , decode( tc.DATA_SCALE , NULL, 126 , nvl( tc.DATA_PRECISION, 38 ) ) , 'FLOAT' , tc.DATA_PRECISION , 'DATE' , 19 , tc.DATA_LENGTH ) COLUMN_SIZE , decode( tc.DATA_TYPE , 'LONG RAW' , 2147483647 , 'LONG' , 2147483647 , 'CLOB' , 2147483647 , 'NCLOB' , 2147483647 , 'BLOB' , 2147483647 , 'BFILE' , 2147483647 , 'NUMBER' , nvl( tc.DATA_PRECISION, 38 ) + 2 , 'FLOAT' , 8 -- ? , 'DATE' , 16 , tc.DATA_LENGTH ) BUFFER_LENGTH , decode( tc.DATA_TYPE , 'DATE' , 0 , tc.DATA_SCALE ) DECIMAL_DIGITS -- ... , decode( tc.DATA_TYPE , 'FLOAT' , 2 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 2, 10 ) , NULL ) NUM_PREC_RADIX , decode( tc.NULLABLE , 'Y' , 1 , 'N' , 0 , NULL ) NULLABLE , cc.COMMENTS REMARKS , tc.DATA_DEFAULT COLUMN_DEF -- Column is LONG! , decode( tc.DATA_TYPE , 'MLSLABEL' , -9106 , 'ROWID' , -9104 , 'UROWID' , -9104 , 'BFILE' , -4 -- 31? , 'LONG RAW' , -4 , 'RAW' , -3 , 'LONG' , -1 , 'UNDEFINED', 0 , 'CHAR' , 1 , 'NCHAR' , 1 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 8, 3 ) , 'FLOAT' , 8 , 'VARCHAR2' , 12 , 'NVARCHAR2', 12 , 'BLOB' , 30 , 'CLOB' , 40 , 'NCLOB' , 40 , 'DATE' , 9 -- not 93! , NULL ) SQL_DATA_TYPE -- ... , decode( tc.DATA_TYPE , 'DATE' , 3 , NULL ) SQL_DATETIME_SUB -- ... , to_number( NULL ) CHAR_OCTET_LENGTH -- TODO , tc.COLUMN_ID ORDINAL_POSITION , decode( tc.NULLABLE , 'Y' , 'YES' , 'N' , 'NO' , NULL ) IS_NULLABLE FROM ALL_TAB_COLUMNS tc , ALL_COL_COMMENTS cc WHERE tc.OWNER = cc.OWNER AND tc.TABLE_NAME = cc.TABLE_NAME AND tc.COLUMN_NAME = cc.COLUMN_NAME ) WHERE 1 = 1 SQL my @BindVals = (); while ( my ( $k, $v ) = each %$attr ) { if ( $v ) { $SQL .= " AND $k LIKE ? ESCAPE '\\'\n"; push @BindVals, $v; } } $SQL .= " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION\n"; my $sth = $dbh->prepare( $SQL ) or return undef; $sth->execute( @BindVals ) or return undef; $sth; } sub get_primary_key_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_primary_key_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my($catalog, $schema, $table) = @_; if (ref $catalog eq 'HASH') { ($schema, $table) = @$catalog{'TABLE_SCHEM','TABLE_NAME'}; $catalog = undef; } my $SQL = <<'SQL'; SELECT * FROM ( SELECT NULL TABLE_CAT , c.OWNER TABLE_SCHEM , c.TABLE_NAME TABLE_NAME , c.COLUMN_NAME COLUMN_NAME , c.POSITION KEY_SEQ , c.CONSTRAINT_NAME PK_NAME FROM ALL_CONSTRAINTS p , ALL_CONS_COLUMNS c WHERE p.OWNER = c.OWNER AND p.TABLE_NAME = c.TABLE_NAME AND p.CONSTRAINT_NAME = c.CONSTRAINT_NAME AND p.CONSTRAINT_TYPE = 'P' ) WHERE TABLE_SCHEM = ? AND TABLE_NAME = ? ORDER BY TABLE_SCHEM, TABLE_NAME, KEY_SEQ SQL #warn "@_\n$Sql ($schema, $table)"; my $sth = $dbh->prepare($SQL) or return undef; $sth->execute($schema, $table) or return undef; $sth; } sub get_foreign_key_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_foreign_key_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my $attr = ( ref $_[0] eq 'HASH') ? $_[0] : { 'UK_TABLE_SCHEM' => $_[1],'UK_TABLE_NAME ' => $_[2] ,'FK_TABLE_SCHEM' => $_[4],'FK_TABLE_NAME ' => $_[5] }; my $SQL = <<'SQL'; # XXX: DEFERABILITY SELECT * FROM ( SELECT to_char( NULL ) UK_TABLE_CAT , uk.OWNER UK_TABLE_SCHEM , uk.TABLE_NAME UK_TABLE_NAME , uc.COLUMN_NAME UK_COLUMN_NAME , to_char( NULL ) FK_TABLE_CAT , fk.OWNER FK_TABLE_SCHEM , fk.TABLE_NAME FK_TABLE_NAME , fc.COLUMN_NAME FK_COLUMN_NAME , uc.POSITION ORDINAL_POSITION , 3 UPDATE_RULE , decode( fk.DELETE_RULE, 'CASCADE', 0, 'RESTRICT', 1, 'SET NULL', 2, 'NO ACTION', 3, 'SET DEFAULT', 4 ) DELETE_RULE , fk.CONSTRAINT_NAME FK_NAME , uk.CONSTRAINT_NAME UK_NAME , to_char( NULL ) DEFERABILITY , decode( uk.CONSTRAINT_TYPE, 'P', 'PRIMARY', 'U', 'UNIQUE') UNIQUE_OR_PRIMARY FROM ALL_CONSTRAINTS uk , ALL_CONS_COLUMNS uc , ALL_CONSTRAINTS fk , ALL_CONS_COLUMNS fc WHERE uk.OWNER = uc.OWNER AND uk.CONSTRAINT_NAME = uc.CONSTRAINT_NAME AND fk.OWNER = fc.OWNER AND fk.CONSTRAINT_NAME = fc.CONSTRAINT_NAME AND uk.CONSTRAINT_TYPE IN ('P','U') AND fk.CONSTRAINT_TYPE = 'R' AND uk.CONSTRAINT_NAME = fk.R_CONSTRAINT_NAME AND uk.OWNER = fk.R_OWNER AND uc.POSITION = fc.POSITION ) WHERE 1 = 1 SQL my @BindVals = (); while ( my ( $k, $v ) = each %$attr ) { if ( $v ) { $SQL .= " AND $k = ?\n"; push @BindVals, $v; } } $SQL .= " ORDER BY UK_TABLE_SCHEM, UK_TABLE_NAME, FK_TABLE_SCHEM, FK_TABLE_NAME, ORDINAL_POSITION\n"; my $sth = $dbh->prepare( $SQL ) or return undef; $sth->execute( @BindVals ) or return undef; $sth; } sub _get_oracle_major_server_version { my $self = shift; unless (exists $self->{'__ora_major_server_version'}) { my $dbh = $self->get_default_handle(); my @data = $dbh->selectrow_arrayref('select version from v$instance'); $self->{'__ora_major_server_version'} = (split(/\./, $data[0]->[0]))[0]; } return $self->{'__ora_major_server_version'}; } sub cast_for_data_conversion { my($class, $left_type, $right_type, $operator, $sql_clause) = @_; my @retval = ('%s','%s'); # compatible types if ($left_type->isa($right_type) or $right_type->isa($left_type) ) { return @retval; } if (! $left_type->isa('UR::Value::Text') and ! $right_type->isa('UR::Value::Text') ) { # We only support cases where one is a string, for now # hopefully the DB can sort it out return @retval; } # Oracle can auto-convert strings into numbers and dates in the 'where' # clause, but has issues in joins if ($sql_clause eq 'where') { return @retval; } # Figure out which one is the non-string my($data_type, $i) = $left_type->isa('UR::Value::Text') ? ( $right_type, 1) : ( $left_type, 0); if ($data_type->isa('UR::Value::Number')) { $retval[$i] = q{to_char(%s)}; } elsif ($data_type->isa('UR::Value::Timestamp')) { # These time formats shoule match what's given in init_created_handle $retval[$i] = qq{to_char(%s, '$TIMESTAMP_FORMAT')}; } elsif ($data_type->isa('UR::Value::DateTime')) { $retval[$i] = qq{to_char(%s, '$DATE_FORMAT')}; } else { @retval = $class->SUPER::cast_for_data_conversion($left_type, $right_type); } return @retval; } sub _vendor_data_type_for_ur_data_type { return ( TEXT => 'VARCHAR2', STRING => 'VARCHAR2', BOOLEAN => 'INTEGER', __default__ => 'VARCHAR2', shift->SUPER::_vendor_data_type_for_ur_data_type(), ); }; 1; =pod =head1 NAME UR::DataSource::Oracle - Oracle specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the Oracle-specific methods necessary for interacting with Oracle databases =head1 SEE ALSO L, L =cut Pg.pm100664023532023421 1572112544604516 15715 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::Pg; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Pg', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "Pg" } #sub server { # my $self = shift->_singleton_object(); # $self->_init_database; # return $self->_database_file_path; #} sub owner { shift->_singleton_object->login } #sub login { # undef #} # #sub auth { # undef #} sub _default_sql_like_escape_string { return '\\\\' }; sub _format_sql_like_escape_string { my $class = shift; my $escape = shift; return "E'$escape'"; } sub can_savepoint { 1;} sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; $dbh->pg_savepoint($sp_name); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; $dbh->pg_rollback_to($sp_name); } *_init_created_dbh = \&init_created_handle; sub init_created_handle { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(pg_|sql_)/; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; # we may need to change how this db handle is gotten my $dbh = $self->get_default_handle; my($new_id) = $dbh->selectrow_array("SELECT nextval('$sequence_name')"); if ($dbh->err) { die "Failed to prepare SQL to generate a column id from sequence: $sequence_name.\n" . $dbh->errstr . "\n"; return; } return $new_id; } # The default for PostgreSQL's serial datatype is to create a sequence called # tablename_columnname_seq sub _get_sequence_name_for_table_and_column { my($self,$table_name, $column_name) = @_; return sprintf("%s_%s_seq",$table_name, $column_name); } sub get_bitmap_index_details_from_data_dictionary { # FIXME Postgres has bitmap indexes, but we don't support them yet. See the Oracle # datasource module for details about how to get it working return []; } sub get_unique_index_details_from_data_dictionary { my($self, $owner_name, $table_name) = @_; my $sql = qq( SELECT c_index.relname, a.attname FROM pg_catalog.pg_class c_table JOIN pg_catalog.pg_index i ON i.indrelid = c_table.oid JOIN pg_catalog.pg_class c_index ON c_index.oid = i.indexrelid JOIN pg_catalog.pg_attribute a ON a.attrelid = c_index.oid JOIN pg_catalog.pg_namespace n ON c_table.relnamespace = n.oid WHERE c_table.relname = ? AND n.nspname = ? and (i.indisunique = 't' or i.indisprimary = 't') and i.indisvalid = 't' ); my $dbh = $self->get_default_handle(); return undef unless $dbh; my $sth = $dbh->prepare($sql); return undef unless $sth; #my $db_owner = $self->owner(); # We should probably do something with the owner/schema $sth->execute($table_name, $owner_name); my $ret; while (my $data = $sth->fetchrow_hashref()) { $ret->{$data->{'relname'}} ||= []; push @{ $ret->{ $data->{'relname'} } }, $data->{'attname'}; } return $ret; } my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'SMALLINT' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'SERIAL' => ['Integer', undef], 'TEXT' => ['Text', undef], 'BYTEA' => ['Blob', undef], 'CHARACTER VARYING' => ['Text', undef], 'TIMESTAMP WITHOUT TIME ZONE' => ['DateTime', undef], 'NUMERIC' => ['Number', undef], 'DOUBLE PRECISION' => ['Number', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; $type = $class->normalize_vendor_type($type); my $urtype = $ur_data_type_for_vendor_data_type{$type}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } sub _vendor_data_type_for_ur_data_type { return ( BOOLEAN => 'BOOLEAN', XML => 'XML', shift->SUPER::_vendor_data_type_for_ur_data_type(), ); } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; for (my $n = 0; $n < @$column_objects; $n++) { next unless defined ($column_objects->[$n]); # No metaDB info for this one if (uc($column_objects->[$n]->data_type) eq 'BLOB') { require DBD::Pg; $sth->bind_param($n+1, undef, { pg_type => DBD::Pg::PG_BYTEA() }); } } } my $DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'; my $TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.US'; sub cast_for_data_conversion { my($class, $left_type, $right_type, $operator, $sql_clause) = @_; my @retval = ('%s','%s'); # compatible types if ($left_type->isa($right_type) or $right_type->isa($left_type) ) { return @retval; } # So far, the only casting is to support using 'like' and one or both are strings if ($operator ne 'like' or ( ! $left_type->isa('UR::Value::Text') and ! $right_type->isa('UR::Value::Text') ) ) { return @retval; } # Figure out which one is the non-string my($data_type, $i) = $left_type->isa('UR::Value::Text') ? ( $right_type, 1) : ( $left_type, 0); if ($data_type->isa('UR::Value::Timestamp')) { $retval[$i] = qq{to_char(%s, '$TIMESTAMP_FORMAT')}; } elsif ($data_type->isa('UR::Value::DateTime')) { $retval[$i] = qq{to_char(%s, '$DATE_FORMAT')}; } else { @retval = $class->SUPER::cast_for_data_conversion($left_type, $right_type, $operator); } return @retval; } sub _resolve_order_by_clause_for_column { my($self, $column_name, $query_plan, $property_meta) = @_; my $column_clause = $self->SUPER::_resolve_order_by_clause_for_column($column_name, $query_plan); my $is_text_type = $property_meta->is_text; if ($is_text_type) { # Tell the DB to sort the same order as Perl's cmp $column_clause .= q( COLLATE "C"); } return $column_clause; } sub _assure_schema_exists_for_table { my($self, $table_name, $dbh) = @_; $dbh ||= $self->get_default_handle; my ($schema_name, undef) = $self->_extract_schema_and_table_name($table_name); if ($schema_name) { my $exists = $dbh->selectrow_array("SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?;", undef, $schema_name); unless ($exists) { $dbh->do("CREATE SCHEMA $schema_name") or Carp::croak("Could not create schema $schema_name: " . $dbh->errstr); } } } 1; =pod =head1 NAME UR::DataSource::Pg - PostgreSQL specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the PostgreSQL-specific methods necessary for interacting with PostgreSQL databases =head1 SEE ALSO L, L =cut False.pm100664023532023421 41112544604516 20470 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/Pg/Operatoruse strict; use warnings; package UR::DataSource::Pg::Operator::False; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = qq(( $expr_sql IS NULL or ${expr_sql}::text = '0' or ${expr_sql}::text = '' )); return ($sql); } 1; True.pm100664023532023421 42012544604516 20355 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/Pg/Operatoruse strict; use warnings; package UR::DataSource::Pg::Operator::True; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = qq(( $expr_sql IS NOT NULL and ${expr_sql}::text != '0' and ${expr_sql}::text != '' )); return ($sql); } 1; QueryPlan.pm100664023532023421 32013712544604516 17307 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::QueryPlan; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; # this class is an evolving attempt to formalize # the blob of cached value used for query construction class UR::DataSource::QueryPlan { is => 'UR::Value', id_by => [ rule_template => { is => 'UR::BoolExpr::Template', id_by => ['subject_class_name','logic_type','logic_detail','constant_values_id'] }, data_source => { is => 'UR::DataSource', id_by => 'data_source_id' }, ], has_transient => [ _is_initialized => { is => 'Boolean' }, needs_further_boolexpr_evaluation_after_loading => { is => 'Boolean' }, # data tracked for the whole query by property,alias,join_id _delegation_chain_data => { is => 'HASH' }, _alias_data => { is => 'HASH' }, _join_data => { is => 'HASH' }, # the old $alias_num _alias_count => { is => 'Number' }, # the old @sql_joins _db_joins => { is => 'ARRAY' }, # the new @obj_joins _obj_joins => { is => 'ARRAY' }, # the old all_table_properties, which has a small array of loading info _db_column_data => { is => 'ARRAY' }, # the old hashes by the same names _group_by_property_names => { is => 'HASH' }, _order_by_property_names => { is => 'HASH' }, _sql_filters => { is => 'ARRAY' }, _sql_params => { is => 'ARRAY' }, lob_column_names => {}, lob_column_positions => {}, query_config => {}, post_process_results_callback => {}, select_clause => {}, select_hint => {}, from_clause => {}, where_clause => {}, connect_by_clause => {}, group_by_clause => {}, order_by_columns => {}, order_by_non_column_data => {}, # flag that's true if asked to order_by something not in the data source sql_params => {}, filter_specs => {}, property_names_in_resultset_order => {}, rule_template_id => {}, rule_template_id_without_recursion_desc => {}, rule_template_without_recursion_desc => {}, joins => {}, recursion_desc => {}, recurse_property_on_this_row => {}, recurse_property_referencing_other_rows => {}, recurse_resolution_by_iteration => {}, # For data sources that don't support recursive queries joins_across_data_sources => {}, # context _resolve_query_plan_for_ds_and_bxt loading_templates => {}, class_name => {}, rule_matches_all => {}, rule_template_is_id_only => {}, sub_typing_property => {}, class_table_name => {}, rule_template_specifies_value_for_subtype => {}, sub_classification_meta_class_name => {}, ] }; sub _load { my $class = shift; my $rule = shift; # See if the requested object is loaded. my @loaded = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); return $class->context_return(@loaded) if @loaded; # Auto generate the object on the fly. my $id = $rule->value_for_id; unless (defined $id) { #$DB::single = 1; Carp::croak "No id specified for loading members of an infinite set ($class)!" } my $class_meta = $class->__meta__; my @p = (id => $id); if (my $alt_ids = $class_meta->{id_by}) { if (@$alt_ids == 1) { push @p, $alt_ids->[0] => $id; } else { my ($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule); push @p, $rule->params_list; } } my $obj = $UR::Context::current->_construct_object($class, @p); if (my $method_name = $class_meta->sub_classification_method_name) { my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule); my $sub_class_name = $obj->$method_name; if ($sub_class_name ne $class) { # delegate to the sub-class to create the object $UR::Context::current->_abandon_object($obj); $obj = $UR::Context::current->_construct_object($sub_class_name,$rule); $obj->__signal_change__("load"); return $obj; } # fall through if the class names match } $obj->__signal_change__("load"); return $obj; } # these hash keys are probably removable # because they are not above, they will be deleted if _init sets them # this exists primarily as a cleanup target list my @extra = qw( id_properties direct_table_properties all_table_properties sub_classification_method_name subclassify_by properties_meta_in_resultset_order all_properties rule_specifies_id all_id_property_names id_property_sorter properties_for_params first_table_name base_joins parent_class_objects ); sub _init { my $self = shift; Carp::confess("already initialized???") if $self->_is_initialized; # We could have this sub-classify by data source type, but right # now it's conditional logic because we'll likely remove the distinctions. # This will work because we'll separate out the ds-specific portion # and call methods on the DS to get that part. my $ds = $self->data_source; if ($ds->isa("UR::DataSource::RDBMS")) { $self->_init_light(); $self->_init_rdbms(); } elsif ($ds->isa('UR::DataSource::Filesystem')) { $self->_init_core(); $self->_init_filesystem(); } else { # Once all callers are using the API for this we won't need "_init". $self->_init_core(); $self->_init_default() if $ds->isa("UR::DataSource::Default"); #$self->_init_remote_cache() if $ds->isa("UR::DataSource::RemoteCache"); } # This object is currently still used as a hashref, but the properties # are a declaration of the part of the hashref data we are still dependent upon. # This removes the other properties to ensure this is the case. # Next steps are to clean up the code below to not produce the data, # then this loop can throw an exception if extra untracked data is found. for my $key (keys %$self) { next if $self->can($key); delete $self->{$key}; } $self->_is_initialized(1); return $self; } sub _determine_complete_order_by_list { my($self, $rule_template, $class_data, $db_property_data) = @_; my $class_meta = $rule_template->subject_class_name->__meta__; my $order_by_columns = $class_data->{order_by_columns} || []; my $order_by = $rule_template->order_by; my $ds = $self->data_source; my %order_by_property_names; my $order_by_non_column_data; if ($order_by) { my %db_property_data_map = map { $_->[1]->property_name => $_ } @$db_property_data; # we only pull back columns we're ordering by if there is ordering happening my %is_descending; my @column_data; for my $name (@$order_by) { my $order_by_prop = $name; if ($order_by_prop =~ m/^(-|\+)(.*)$/) { $order_by_prop = $2; $is_descending{$order_by_prop} = $1 eq '-'; } my($order_by_prop_meta) = $class_meta->_concrete_property_meta_for_class_and_name($order_by_prop); unless ($order_by_prop_meta) { Carp::croak("Cannot order by '$name': Class " . $class_meta->class_name . " has no property named '$order_by_prop'"); } $name = ( $is_descending{$order_by_prop} ? '-' : '' ) . $order_by_prop_meta->property_name; if ($order_by_property_names{$name} = $db_property_data_map{$order_by_prop_meta->property_name}) { # yes, single = push @column_data, $order_by_property_names{$name}; my $table_column_names = $ds->_select_clause_columns_for_table_property_data($column_data[-1]); $is_descending{$table_column_names->[0]} = $is_descending{$order_by_prop}; # copy for table.column designation $order_by_property_names{$table_column_names->[0]} = $order_by_property_names{$name}; } else { $order_by_non_column_data = 1; } } if (@column_data) { my $additional_order_by_columns = $ds->_select_clause_columns_for_table_property_data(@column_data); # Strip out columns named in the original $order_by_columns list that now appear in the # additional order by list so we don't duplicate columns names, and the additional columns # appear earlier in the list my %additional_order_by_columns = map { $_ => 1 } @$additional_order_by_columns; my @existing_order_by_columns = grep { ! $additional_order_by_columns{$_} } @$order_by_columns; $order_by_columns = [ map { $is_descending{$_} ? '-'. $_ : $_ } ( @$additional_order_by_columns, @existing_order_by_columns ) ]; } } $self->_order_by_property_names(\%order_by_property_names); return ($order_by_columns, $order_by_non_column_data); } sub _init_rdbms { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; #my $first_table_name = $class_data->{first_table_name}; #my $id_property_sorter = $class_data->{id_property_sorter}; #my @lob_column_names = @{ $class_data->{lob_column_names} }; my @lob_column_positions = @{ $class_data->{lob_column_positions} }; #my $query_config = $class_data->{query_config}; #my $post_process_results_callback = $class_data->{post_process_results_callback}; #my $class_table_name = $class_data->{class_table_name}; # individual template based my $hints = $rule_template->hints; my %hints = map { $_ => 1 } @$hints; my $order_by = $rule_template->order_by; my $group_by = $rule_template->group_by; my $limit = $rule_template->limit; my $aggregate = $rule_template->aggregate; my $recursion_desc = $rule_template->recursion_desc; my ($first_table_name, @db_joins) = _resolve_db_joins_for_inheritance($class_meta); $self->_db_joins(\@db_joins); $self->_obj_joins([]); # an array of arrays, containing $table_name, $column_name, $alias, $object_num # as joins are done we extend this, and then condense it into object fabricators my @db_property_data = @{ $class_data->{all_table_properties} }; my %group_by_property_names; if ($group_by) { # we only pull back columns we're grouping by or aggregating if there is grouping happening for my $name (@$group_by) { unless ($class_name->can($name)) { Carp::croak("Cannot group by '$name': Class $class_name has no property/method by that name"); } $group_by_property_names{$name} = 1; } for my $data (@db_property_data) { my $name = $data->[1]->property_name; if ($group_by_property_names{$name}) { $group_by_property_names{$name} = $data; } } @db_property_data = grep { ref($_) } values %group_by_property_names; } my($order_by_columns, $order_by_non_column_data) = $self->_determine_complete_order_by_list($rule_template, $class_data,\@db_property_data); $self->_db_column_data(\@db_property_data); $self->_group_by_property_names(\%group_by_property_names); # Find out what delegated properties we'll be dealing with my @sql_filters; my @delegated_properties; do { my %filters = map { $_ => $rule_template->operator_for($_) } grep { substr($_,0,1) ne '-' } $rule_template->_property_names; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } # Remove the flag for descending/ascending sort my @order_by_properties = $order_by ? @$order_by : (); s/^-|\+// foreach @order_by_properties; my %properties_involved = map { $_ => 1 } keys(%filters), ($hints ? @$hints : ()), @order_by_properties, ($group_by ? @$group_by : ()); my @properties_involved = sort keys(%properties_involved); my @errors; while (my $property_name = shift @properties_involved) { if (index($property_name,'.') != -1) { push @delegated_properties, $property_name; next; } my (@pmeta) = $class_meta->property_meta_for_name($property_name); unless (@pmeta) { if ($class_name->can($property_name)) { # method, not property next; } else { push @errors, "Class ".$class_meta->id." has no property or method named '$property_name'"; next; } } # For each property in this list, go up the inheritance and find the right property # to query on. Give priority to properties that actually have columns FIND_PROPERTY_WITH_COLUMN: foreach my $pmeta ( @pmeta ) { foreach my $candidate_class ( $class_meta->all_class_metas ) { my $candidate_prop_meta = UR::Object::Property->get(class_name => $candidate_class->class_name, property_name => $property_name); next unless $candidate_prop_meta; if ($candidate_prop_meta->column_name) { $pmeta = $candidate_prop_meta; next FIND_PROPERTY_WITH_COLUMN; } } } my $property = $pmeta[0]; my $table_name = $property->class_meta->first_table_name; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); if ($property->can("expr_sql")) { unless ($table_name) { $ds->warning_message("Property '$property_name' of class '$class_name' can 'expr_sql' but has no table!"); next; } my $expr_sql = $property->expr_sql; if (exists $filters{$property_name}) { my @coercion = $self->data_source->cast_for_data_conversion( $property->_data_type_as_class_name, 'UR::Value::String', # We can't know here what the type should be $operator, 'where'); push @sql_filters, $table_name => { # cheap hack of prefixing with a whitespace differentiates # from a regular column below " " . $expr_sql => { operator => $operator, value_position => $value_position, left_coercion => $coercion[0], right_coercion => $coercion[1], } }; } next; } # If the property is calculate and has a calculate_from list, add the # calculate_from things to the internal hints list, but not the template if ($property->is_calculated and $property->calculate_from) { my $calculate_from = $property->calculate_from; push @properties_involved, @$calculate_from; push @$hints, @$calculate_from; $hints{$_} = 1 foreach @$calculate_from; } if (exists($filters{$property_name}) and $filters{$property_name} eq 'isa') { # RDBMS databases can't do 'isa' $self->needs_further_boolexpr_evaluation_after_loading(1); next; } elsif (my $column_name = $property->column_name) { # normal column: filter on it unless ($table_name) { $ds->warning_message("Property '$property_name' of class '$class_name' has column '$column_name' but has no table!"); next; } if (exists $filters{$property_name}) { my @coercion = $self->data_source->cast_for_data_conversion( $property->_data_type_as_class_name, 'UR::Value::String', # We can't know here what the type should be $operator, 'where'); push @sql_filters, $table_name => { $column_name => { operator => $operator, value_position => $value_position, left_coercion => $coercion[0], right_coercion => $coercion[1], } }; } } elsif ($property->is_delegated) { push @delegated_properties, $property->property_name; } elsif ( ! exists($hints{$property_name}) or exists($filters{$property_name}) ) { $self->needs_further_boolexpr_evaluation_after_loading(1); } else { next; } } # end of properties in the expression which control the query content if (@errors) { my $class_name = $class_meta->class_name; $ds->error_message("ERRORS PROCESSING PARAMTERS: (" . join("\n", @errors) . ") used to generate SQL for $class_name!"); #print Data::Dumper::Dumper($rule_template); Carp::croak("Can't continue"); } }; my $object_num = 0; $self->_alias_count(0); my %hints_included; my @select_hint; # FIXME - this needs to be broken out into delegated-property-join-resolver # and inheritance-join-resolver methods that can be called recursively. # It would better encapsulate what's going on and avoid bugs with complicated # get()s # one iteration per target value involved in the query, # including values needed for filtering, ordering, grouping, and hints (selecting more) # these "properties" may be a single property name or an ad-hoc "chain" DELEGATED_PROPERTY: for my $delegated_property (sort @delegated_properties) { my $property_name = $delegated_property; my $delegation_chain_data = $self->_delegation_chain_data || $self->_delegation_chain_data({}); $delegation_chain_data->{"__all__"}{table_alias} = {}; $delegation_chain_data->{"__all__"}{class_alias} = { $first_table_name => $class_meta }; my ($final_accessor, $is_optional, @joins) = _resolve_object_join_data_for_property_chain($rule_template,$property_name,$property_name); # when there is no "final_accessor" it often means we have an object-accessor in a hint # we want that to go through the join process, and only be left out at filter construction time #unless ($final_accessor) { #$self->needs_further_boolexpr_evaluation_after_loading(1); #next; #} # this is gathered here and used below, but previously was gathered internally to the methods which take it # since it is no longer needed directly in this method it might be refactored into the places which use it my %ds_for_class; for my $join (@joins) { my $source_class_object = $join->{'source_class'}->__meta__; my ($source_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($source_class_object, $rule_template); $ds_for_class{$join->{'source_class'}} = $source_data_source; my $foreign_class_object = $join->{'foreign_class'}->__meta__; my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); $ds_for_class{$join->{'foreign_class'}} = $foreign_data_source; } # Splice out joins that go through a UR::Value class and back out to the DB, since UR::Value-types # don't get stored in the DB # TODO: move this into the join creation logic for (my $i = 0; $i < @joins; $i++) { if ( $i < $#joins and ( # db -> UR::Value -> db : shortcut $joins[$i]->{'foreign_class'}->isa('UR::Value') and $joins[$i+1]->{'source_class'}->isa('UR::Value') #and $joins[$i]->{'foreign_class'}->isa($joins[$i+1]->{'source_class'}) ## remove this? ) ) { my $fixed_join = UR::Object::Join->_get_or_define( source_class => $joins[$i]->{'source_class'}, source_property_names => $joins[$i]->{'source_property_names'}, foreign_class => $joins[$i+1]->{'foreign_class'}, foreign_property_names => $joins[$i+1]->{'foreign_property_names'}, is_optional => $joins[$i]->{'is_optional'}, id => $joins[$i]->{id} . "->" . $joins[$i+1]->{id}); if ($joins[$i+1]->{where}) { # If there's a where involved, it will always be on the second thing, # where the foreign_class is NOT a UR::Value $fixed_join->{where} = $joins[$i+1]->{where}; } splice(@joins, $i, 2, $fixed_join); } } if (@joins and $joins[-1]{foreign_class}->isa("UR::Value")) { # the final join in a chain is often the link between a primitive value # and the UR::Value subclass into which it falls ...irrelevent for db joins $final_accessor = $joins[-1]->source_property_names->[0]; pop @joins; next DELEGATED_PROPERTY unless @joins; } my $last_class_object_excluding_inherited_joins; my $alias_for_property_value; # one iteration per table between the start table and target while (my $object_join = shift @joins) { $object_num++; my @joins_for_object = ($object_join); # one iteration per layer of inheritance for this object # or per case of a join having additional filtering my $current_inheritance_depth_for_this_target_join = 0; while (my $join = shift @joins_for_object) { my $where = $join->{where}; $current_inheritance_depth_for_this_target_join++; my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; if ($foreign_class_object->join_hint and !($hints_included{$foreign_class_name}++)) { push @select_hint, $foreign_class_object->join_hint; } if (not exists $ds_for_class{$foreign_class_name}) { # error: we should have at least a key with an empty value if we tried to find the ds die "no data source key for $foreign_class_name when adding a join?" } my $ds = $ds_for_class{$foreign_class_name}; if (not $ds) { # no ds for the next piece of data: we will have to resolve this on the client side # this is where things may get slow if the query is insufficiently filtered $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } my $alias = $self->_add_join( $delegated_property, $join, $object_num, $is_optional, $final_accessor, $ds_for_class{$foreign_class_name}, ); if (not $alias) { # unable to add a join for another reason # TODO: is the above the only valid case of a join being impossible? # Can we remove this? $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } # set these for after all of the joins are done my $last_class_name = $foreign_class_name; my $last_class_object = $foreign_class_object; # on the first iteration, we figure out the remaining inherited iterations # if there is inheritance to do, unshift those onto the stack ahead of other things if ($current_inheritance_depth_for_this_target_join == 1) { if ($final_accessor and $last_class_object->property_meta_for_name($final_accessor)) { $last_class_object_excluding_inherited_joins = $last_class_object; } my @parents = grep { $_->table_name } $foreign_class_object->ancestry_class_metas; if (@parents) { my @last_id_property_names = $foreign_class_object->id_property_names; for my $parent (@parents) { my @parent_id_property_names = $parent->id_property_names; die if @parent_id_property_names > 1; my $parent_join_foreign_class_name = $parent->class_name; my $inheritance_join = UR::Object::Join->_get_or_define( source_class => $last_class_name, source_property_names => [@last_id_property_names], # we change content below foreign_class => $parent_join_foreign_class_name, foreign_property_names => \@parent_id_property_names, is_optional => $is_optional, id => "${last_class_name}::" . join(',',@last_id_property_names), ); unshift @joins_for_object, $inheritance_join; @last_id_property_names = @parent_id_property_names; $last_class_name = $foreign_class_name; my $foreign_class_object = $parent_join_foreign_class_name->__meta__; my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); $ds_for_class{$parent_join_foreign_class_name} = $foreign_data_source; } next; } } if (!@joins and not $alias_for_property_value) { # we are out of joins for this delegated property # setting $alias_for_property_value helps map to exactly where we do real filter/order/etc. my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object); if ($final_accessor and grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} } ) { $alias_for_property_value = $alias; #print "found alias for $property_name on $foreign_class_name: $alias\n"; } else { # The thing we're joining to isn't a database-backed column (maybe calculated?) $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } } } # next join in the inheritance for this object } # next join across objects from the query subject to the delegated property target # done adding any new joins for this delegated property/property-chain # now see if anything in the where-clause needs to filter on the item joined-to my $value_position = $rule_template->value_position_for_property_name($property_name); if (defined $value_position) { # this property _is_ used to filter results if (not $final_accessor) { # on the client side :( $self->needs_further_boolexpr_evaluation_after_loading(1); next; } else { # at the database level :) my $final_accessor_property_meta = $last_class_object_excluding_inherited_joins->property_meta_for_name($final_accessor); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object_excluding_inherited_joins->class_name . " while resolving joins for property '" . $delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $self->needs_further_boolexpr_evaluation_after_loading(1); next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); unless ($alias_for_property_value) { die "No alias found for $property_name?!"; } my @coercion = $self->data_source->cast_for_data_conversion( $final_accessor_property_meta->_data_type_as_class_name, 'UR::Value::String', # We can't know here what the type should be $operator, 'where'); push @sql_filters, $alias_for_property_value => { $sql_lvalue => { operator => $operator, value_position => $value_position, left_coercion => $coercion[0], right_coercion => $coercion[1], } }; } } } # next delegated property # the columns to query my $db_property_data = $self->_db_column_data; # the following two sets of variables hold the net result of the logic my $select_clause; my $from_clause; my $connect_by_clause; my $group_by_clause; # Build the SELECT clause explicitly. $select_clause = $ds->_select_clause_for_table_property_data(@$db_property_data); # Oracle places group_by in a comment in the select unshift(@select_hint, $class_meta->select_hint) if $class_meta->select_hint; # Build the FROM clause base. # Add joins to the from clause as necessary, then $from_clause = (defined $first_table_name ? "$first_table_name" : ''); my $cnt = 0; my @sql_params; my @sql_joins = @{ $self->_db_joins }; while (@sql_joins) { my $table_name = shift (@sql_joins); my $condition = shift (@sql_joins); my ($table_alias) = ($table_name =~ /(\S+)\s*$/s); my $join_type; if ($condition->{-is_required}) { $join_type = 'INNER'; } else { $join_type = 'LEFT'; } $from_clause .= "\n$join_type join " . $table_name . " on "; # Restart the counter on each join for the from clause, # but for the where clause keep counting w/o reset. $cnt = 0; for my $column_name (keys %$condition) { next if substr($column_name,0,1) eq '-'; my $linkage_data = $condition->{$column_name}; my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}"); my ($operator, $value_position, $value, $link_table_name, $link_column_name, $left_coercion, $right_coercion) = @$linkage_data{qw/operator value_position value link_table_name link_column_name left_coercion right_coercion/}; $expr_sql = sprintf($right_coercion, $expr_sql) if ($right_coercion); $from_clause .= "\n and " if ($cnt++); if ($link_table_name and $link_column_name) { # the linkage data is a join specifier my $link_sql = "${link_table_name}.${link_column_name}"; $link_sql = sprintf($left_coercion, $link_sql) if ($left_coercion); $from_clause .= "$link_sql = $expr_sql"; } elsif (defined $value_position) { Carp::croak("Joins cannot use variable values currently!"); } else { my ($more_sql, @more_params) = $ds->_extend_sql_for_column_operator_and_value($expr_sql, $operator, $value); if ($more_sql) { $from_clause .= $more_sql; push @sql_params, @more_params; } else { # error return; } } } # next column } # next db join # build the WHERE clause by making a data structure which will be parsed outside of this module # special handling of different size lists, and NULLs, make a completely reusable SQL template very hard. my @filter_specs; while (@sql_filters) { my $table_name = shift (@sql_filters); my $condition = shift (@sql_filters); my ($table_alias) = ($table_name =~ /(\S+)\s*$/s); for my $column_name (keys %$condition) { my $linkage_data = $condition->{$column_name}; my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}"); my ($operator, $value_position, $value, $link_table_name, $link_column_name, $left_coercion, $right_coercion) = @$linkage_data{qw/operator value_position value link_table_name link_column_name left_coercion right_coercion/}; if ($link_table_name and $link_column_name) { # the linkage data is a join specifier Carp::confess("explicit column linkage in where clause?"); #$sql .= "${link_table_name}.${link_column_name} = $expr_sql"; } else { # the linkage data is a value position from the @values list unless (defined $value_position) { Carp::confess("No value position for $column_name in query!"); } $expr_sql = sprintf($left_coercion, $expr_sql); push @filter_specs, [$expr_sql, $operator, $value_position]; } } # next column } # next db filter $connect_by_clause = ''; my $recurse_resolution_by_iteration = 0; if ($recursion_desc) { unless (ref($recursion_desc) eq 'ARRAY') { Carp::croak("Recursion description must be an arrayref with exactly 2 items"); } if (@$recursion_desc != 2) { Carp::croak("Recursion description must contain exactly 2 items; got ".scalar(@$recursion_desc) . ': ' . join(', ',@$recursion_desc)); } # Oracle supports "connect by" queries. if ($ds->does_support_recursive_queries eq 'connect by') { my ($this,$prior) = @{ $recursion_desc }; my $this_property_meta = $class_meta->property_meta_for_name($this); unless ($this_property_meta) { Carp::croak("Class ".$class_meta->class_name." has no property named '$this', named in the recursion description"); } my $prior_property_meta = $class_meta->property_meta_for_name($prior); unless ($prior_property_meta) { Carp::croak("Class ".$class_meta->class_name." has no property named '$prior', named in the recursion description"); } my $this_class_meta = $this_property_meta->class_meta; my $prior_class_meta = $prior_property_meta->class_meta; my $this_table_name = $this_class_meta->table_name; unless ($this_table_name) { Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$this', named in the recursion description"); } my $prior_table_name = $prior_class_meta->table_name; unless ($prior_table_name) { Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$prior', named in the recursion description"); } my $this_column_name = $this_property_meta->column_name || $this; my $prior_column_name = $prior_property_meta->column_name || $prior; $connect_by_clause = "connect by $this_table_name.$this_column_name = prior $prior_table_name.$prior_column_name\n"; } else { $recurse_resolution_by_iteration = 1; } } my @property_names_in_resultset_order; for my $property_meta_array (@$db_property_data) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } # this is only used when making a real instance object instead of a "set" my $per_object_in_resultset_loading_detail; unless ($group_by) { $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@$db_property_data, $self->_obj_joins); } if ($group_by) { # when grouping, we're making set objects instead of regular objects # this means that we re-constitute the select clause and add a group_by clause $group_by_clause = 'group by ' . $select_clause if (scalar(@$group_by)); # Q: - does it even make sense for the user to specify an order_by in the # get() request for Set objects? If so, then we need to concatonate these order_by_columns # with the ones that already exist in $order_by_columns from the class data # A: - yes, because group by means "return a list of subsets", and this lets you sort the subsets $order_by_columns = $ds->_select_clause_columns_for_table_property_data(@$db_property_data); $select_clause .= ', ' if $select_clause; $select_clause .= 'count(*) count'; for my $ag (@$aggregate) { next if $ag eq 'count'; # TODO: translate property names to column names, and skip non-column properties $select_clause .= ', ' . $ag; } unless (@$group_by == @$db_property_data) { print "mismatch table properties vs group by!\n"; } } %$self = ( %$self, # custom for RDBMS select_clause => $select_clause, select_hint => scalar(@select_hint) ? \@select_hint : undef, from_clause => $from_clause, connect_by_clause => $connect_by_clause, group_by_clause => $group_by_clause, order_by_columns => $order_by_columns, order_by_non_column_data => $order_by_non_column_data, filter_specs => \@filter_specs, sql_params => \@sql_params, recurse_resolution_by_iteration => $recurse_resolution_by_iteration, # override defaults in the regular datasource $parent_template_data property_names_in_resultset_order => \@property_names_in_resultset_order, properties_meta_in_resultset_order => $db_property_data, # duplicate?! loading_templates => $per_object_in_resultset_loading_detail, ); my $template_data = $rule_template->{loading_data_cache} = $self; return $self; } sub _init_filesystem { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @db_property_data = @{ $class_data->{all_table_properties} }; my($order_by_columns, $order_by_non_column_data) = $self->_determine_complete_order_by_list($rule_template, $class_data, \@db_property_data); %$self = ( %$self, order_by_columns => $order_by_columns, order_by_non_column_data => $order_by_non_column_data, ); my $template_data = $rule_template->{loading_data_cache} = $self; return $self; } sub _add_join { my ($self, $property_name, $join, $object_num, $is_optional, $final_accessor, $foreign_data_source, ) = @_; my $delegation_chain_data = $self->_delegation_chain_data || $self->_delegation_chain_data({}); my $table_alias = $delegation_chain_data->{"__all__"}{table_alias} ||= {}; my $source_table_and_column_names = $delegation_chain_data->{$property_name}{latest_source_table_and_column_names} ||= []; my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $class_alias = $delegation_chain_data->{"__all__"}{class_alias} ||= {}; if (! %$class_alias and $source_class_object->table_name) { $class_alias->{$source_class_object->table_name} = $source_class_object; } my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $group_by = $rule_template->group_by; #my($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (!$foreign_data_source or ($foreign_data_source ne $ds)) { # FIXME - do something smarter in the future where it can do a join-y thing in memory $self->needs_further_boolexpr_evaluation_after_loading(1); return; } my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object); # This will get filled in during the first pass, and every time after we've successfully # performed a join - ie. that the delegated property points directly to a class/property # that is a real table/column, and not a tableless class or another delegated property my @source_property_names; unless (@$source_table_and_column_names) { @source_property_names = @{ $join->{source_property_names} }; @$source_table_and_column_names = map { if (my($view, $alias) = $ds->parse_view_and_alias_from_inline_view($_->[0])) { # This "table_name" was actually a bit of SQL with an inline view and an alias $_->[0] = $view; $_->[2] = $alias; } $_; } map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::croak("No property $_ for class ".$source_class_object->class_name); } my($table_name,$column_name) = $p->table_and_column_name_for_property(); if ($table_name && $column_name) { [$table_name, $column_name]; } else { #Carp::confess("Can't determine table and column for property $_ in class " . # $source_class_object->class_name); (); } } @source_property_names; } return unless @$source_table_and_column_names; #my @source_property_names = @{ $join->{source_property_names} }; #my ($source_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($source_class_object, @source_property_names); #my @source_column_names = @$fcols; #my @source_property_meta = @$fprops; my @foreign_property_names = @{ $join->{foreign_property_names} }; my ($foreign_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($foreign_class_object, @foreign_property_names); my @foreign_column_names = @$fcols; my @foreign_property_meta = @$fprops; unless (@foreign_column_names) { # all calculated properties: don't try to join any further return; } unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $self->needs_further_boolexpr_evaluation_after_loading(1); return; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $self->needs_further_boolexpr_evaluation_after_loading(1); } my $alias = $self->_get_join_alias($join, $property_name); unless ($alias) { my $alias_num = $self->_alias_count($self->_alias_count+1); my $alias_name = $join->sub_group_label || $property_name; if (substr($alias_name,-1) eq '?') { chop($alias_name) if substr($alias_name,-1) eq '?'; } my $alias_length = length($alias_name)+length($alias_num)+1; my $alias_max_length = 29; if ($alias_length > $alias_max_length) { $alias = substr($alias_name,0,$alias_max_length-length($alias_num)-1); } else { $alias = $alias_name; } $alias =~ s/\./_/g; $alias .= '_' . $alias_num; $self->_set_join_alias($join, $property_name, $alias); if ($foreign_class_object->table_name) { my @extra_db_filters; my @extra_obj_filters; # TODO: when "flatten" correctly feeds the "ON" clause we can remove this # This will crash if the "where" happens to use indirect things my $where = $join->{where}; if ($where) { for (my $n = 0; $n < @$where; $n += 2) { my $key =$where->[$n]; my ($name,$op) = ($key =~ /^(\S+)\s*(.*)/); if(index($name, '-') == 0) { #skip '-order_by', '-hint' and the like for joins next; } #my $meta = $foreign_class_object->property_meta_for_name($name); #my $column = $meta->is_calculated ? (defined($meta->calculate_sql) ? ($meta->calculate_sql) : () ) : ($meta->column_name); my ($table_name, $column_names, $property_metas) = $self->_resolve_table_and_column_data($foreign_class_object, $name); my $column = $column_names->[0]; if (not $column) { Carp::confess("No column for $foreign_class_object->{id} $name? Indirect property flattening must be enabled to use indirect filters in where with via/to."); } my $value = $where->[$n+1]; push @extra_db_filters, $column => { value => $value, ($op ? (operator => $op) : ()) }; push @extra_obj_filters, $name => { value => $value, ($op ? (operator => $op) : ()) }; } } my @db_join_data; for (my $n = 0; $n < @foreign_column_names; $n++) { my $link_table_name = $table_alias->{$source_table_and_column_names->[$n][0]} || $source_table_and_column_names->[$n][2] || $source_table_and_column_names->[$n][0]; my $link_column_name = $source_table_and_column_names->[$n][1]; my $foreign_column_name = $foreign_column_names[$n]; my $link_class_meta = $class_alias->{$link_table_name} || $source_class_object; my $link_property_name = $link_class_meta->property_for_column($link_column_name); # _concrete_property_meta_for_class_and_name returns a list :( # since we're inspecting the joins by their "real" names and not the generic # "id", it will only ever return a 1-element list my($link_prop) = $link_class_meta->_concrete_property_meta_for_class_and_name($link_property_name); my $left_type = $link_prop->_data_type_as_class_name; my $right_type = $foreign_property_meta[$n]->_data_type_as_class_name; my @coercion = $self->data_source->cast_for_data_conversion($left_type, $right_type, '=', 'join'); push @db_join_data, $foreign_column_name => { link_table_name => $link_table_name, link_column_name => $link_column_name, left_coercion => $coercion[0], right_coercion => $coercion[1], }; } $self->_add_db_join( "$foreign_table_name $alias" => { @db_join_data, @extra_db_filters, } ); $self->_add_obj_join( "$alias" => { ( map { $foreign_property_names[$_] => { link_class_name => $source_class_name, link_alias => $table_alias->{$source_table_and_column_names->[$_][0]} # join alias || $source_table_and_column_names->[$_][2] # SQL inline view alias || $source_table_and_column_names->[$_][0], # table_name link_property_name => $source_property_names[$_] } } (0..$#foreign_property_names) ), @extra_obj_filters, } ); # Add all of the columns in the join table to the return list # Note that we increment the object numbers. # Note: we add grouping columns individually instead of in chunks unless ($group_by) { $self->_add_columns( map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = $object_num; $new } @{ $foreign_class_loading_data->{direct_table_properties} } ); } } if ($group_by) { if ($self->_groups_by_property($property_name)) { my ($p) = map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = 0; $new } grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} }; $self->_add_columns($p); } } if ($self->_orders_by_property($property_name)) { my ($p) = map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = 0; $new } grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} }; # ??? what do we do here now with $p? } unless ($is_optional) { # if _any_ part requires this, mark it required $self->_set_alias_required($alias); } } # done adding a new join alias for a join which has not yet been done if ($foreign_class_object->table_name) { $table_alias->{$foreign_table_name} = $alias; $class_alias->{$alias} = $foreign_class_object; @$source_table_and_column_names = (); # Flag that we need to re-derive this at the top of the loop } return $alias; } sub _resolve_table_and_column_data { my ($class, $class_meta, @property_names) = @_; my @property_meta = map { $class_meta->_concrete_property_meta_for_class_and_name($_) } @property_names; my $table_name; my @column_names = map { # TODO: encapsulate if ($_->is_calculated) { if ($_->calculate_sql) { $_->calculate_sql; } else { (); } } else { my $column_name; ($table_name, $column_name) = $_->table_and_column_name_for_property(); $column_name; } } @property_meta; if ($table_name and $table_name =~ /^(.*)\s+(\w+)\s*$/s) { $table_name = $1; } return ($table_name, \@column_names, \@property_meta); } sub _set_join_alias { my ($self, $join, $property_name, $alias) = @_; $self->_join_data->{$join->id}{$property_name}{alias} = $alias; $self->_alias_data({}) unless $self->_alias_data(); $self->_alias_data->{$alias}{join_id} = $join->id; } sub _get_join_alias { my ($self,$join,$property_name) = @_; $self->_join_data({}) unless $self->_join_data(); return $self->_join_data->{$join->id}{$property_name}{alias}; } sub _get_alias_join { my ($self,$alias) = @_; my $alias_data = $self->_alias_data; return if (! $alias_data or ! exists($alias_data->{$alias})); my $join_id = $self->_alias_data->{$alias}{join_id}; UR::Object::Join->get($join_id); } sub _add_db_join { my ($self, $key, $data) = @_; my ($alias) = ($key =~/\w+$/); my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$alias}{db_join} = $data; my $db_joins = $self->_db_joins || $self->_db_joins([]); push @$db_joins, $key, $data; } sub _add_obj_join { my ($self, $key, $data) = @_; Carp::confess() unless ref $data; my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$key}{obj_join} = $data; # the key is the alias here my $obj_joins = $self->_obj_joins || $self->_obj_joins([]); push @$obj_joins, $key, $data; } sub _set_alias_required { my ($self, $alias) = @_; my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$alias}{is_required} = 1; $alias_data->{$alias}{db_join}{-is_required} = 1; $alias_data->{$alias}{obj_join}{-is_required} = 1; } sub _add_columns { my $self = shift; my @new = @_; my $old = $self->_db_column_data; my $pos = @$old; my $lob_column_positions = $self->{lob_column_positions}; my $lob_column_names = $self->{lob_column_names}; for my $class_property (@new) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; my $data_type = $sql_property->data_type || ''; if ($data_type =~ /LOB$/) { push @$lob_column_names, $sql_property->column_name; push @$lob_column_positions, $pos; } $pos++; } push @$old, @new; } # Used by the object fabricator to find out which resultset column a # property's data is stored sub column_index_for_class_property_and_object_num { my($self, $class_name, $property_name, $object_num) = @_; $object_num ||= 0; my $db_column_data = $self->_db_column_data; for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) { if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name and $db_column_data->[$resultset_col]->[1]->property_name eq $property_name and $db_column_data->[$resultset_col]->[3] == $object_num ) { return $resultset_col; } } return undef; } # used by the object fabricator to determine the resultset column # the source property of a join is stored. sub column_index_for_class_and_property_before_object_num { my($self, $class_name, $property_name, $object_num) = @_; return unless $object_num; my $db_column_data = $self->_db_column_data; my $index; for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) { last if ($db_column_data->[$resultset_col]->[3] >= $object_num); if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name and $db_column_data->[$resultset_col]->[1]->property_name eq $property_name ) { $index = $resultset_col; } } return $index; } sub _groups_by_property { my ($self, $property_name) = @_; return $self->_group_by_property_names->{$property_name}; } sub _orders_by_property { my ($self, $property_name) = @_; return $self->_order_by_property_names->{$property_name}; } sub _resolve_db_joins_for_inheritance { my $class_meta = $_[0]; my $first_table_name; my @sql_joins; my $prev_table_name; my $prev_id_column_name; my $prev_property_meta; my @parent_class_objects = $class_meta->ancestry_class_metas; my %seen; for my $co ( $class_meta, @parent_class_objects ) { my $class_name = $co->class_name; next if $seen{$class_name}++; my @id_property_objects = $co->direct_id_property_metas; my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; my $table_name = $co->table_name; if ($table_name) { $first_table_name ||= $table_name; if ($prev_table_name) { die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1; my $prev_table_alias; if ($prev_table_name =~ /.*\s+(\w+)\s*$/) { $prev_table_alias = $1; } else { $prev_table_alias = $prev_table_name; } my @coercion = $co->data_source->cast_for_data_conversion( $prev_property_meta->_data_type_as_class_name, $id_property_objects[0]->_data_type_as_class_name, '=', 'join'); push @sql_joins, $table_name => { $id_property_objects[0]->column_name => { link_table_name => $prev_table_alias, link_column_name => $prev_id_column_name, left_coercion => $coercion[0], right_coercion => $coercion[1], }, -is_required => 1, }; } $prev_table_name = $table_name; $prev_id_column_name = $id_property_objects[0]->column_name; $prev_property_meta = $id_property_objects[0]; } } return ($first_table_name, @sql_joins); } sub _resolve_object_join_data_for_property_chain { my ($rule_template, $property_name) = @_; my $class_meta = $rule_template->subject_class_name->__meta__; my @joins; my $is_optional; my $final_accessor; my @pmeta = $class_meta->_concrete_property_meta_for_class_and_name($property_name); my $last_class_meta = $class_meta; for my $meta (@pmeta) { if (!$meta) { Carp::croak "Can't resolve joins for ".$rule_template->subject_class_name . " property '$property_name': No property metadata found for that class and property_name"; } #id is a special property that we want to look up, but isn't necessarily on a table #so if it aliases another property, we look at that instead if($meta->property_name eq 'id' and $meta->class_name eq 'UR::Object') { my @id_properties = grep {$_->class_name ne 'UR::Object'} $last_class_meta->id_properties; if(@id_properties == 1) { $meta = $id_properties[0]; $last_class_meta = $meta->class_name->__meta__; next; } elsif (@id_properties > 1) { Carp::confess "can't join to class " . $last_class_meta->class_name . " with multiple id properties: @id_properties"; } } if($meta->data_type and $meta->data_type =~ /::/) { $last_class_meta = UR::Object::Type->get($meta->data_type); } else { $last_class_meta = UR::Object::Type->get($meta->class_name); } last unless $last_class_meta; } # we can't actually get this from the joins because # a bunch of optional things can be chained together to form # something non-optional $is_optional = 0; for my $pmeta (@pmeta) { push @joins, $pmeta->_resolve_join_chain(); $is_optional = 1 if $pmeta->is_optional or $pmeta->is_many; } return unless @joins; return ($joins[-1]->{source_name_for_foreign}, $is_optional, @joins) }; sub _init_light { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_properties = @{ $class_data->{all_properties} }; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; my $id_property_sorter = $class_data->{id_property_sorter}; my $sub_typing_property = $class_data->{sub_typing_property}; my $class_table_name = $class_data->{class_table_name}; my $recursion_desc = $rule_template->recursion_desc; my $recurse_property_on_this_row; my $recurse_property_referencing_other_rows; my $recurse_resolution_by_iteration; if ($recursion_desc) { ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc; $recurse_resolution_by_iteration = ! $ds->does_support_recursive_queries; } my $needs_further_boolexpr_evaluation_after_loading; my $is_join_across_data_source; my @sql_params; my @filter_specs; my @property_names_in_resultset_order; my $object_num = 0; # 0-based, usually zero unless there are joins my @filters = $rule_template->_property_names; my %filters = map { $_ => 0 } grep { substr($_,0,1) ne '-' } @filters; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } my ( @sql_joins, @sql_filters, $prev_table_name, $prev_id_column_name, $eav_class, @eav_properties, $eav_cnt, %pcnt, $pk_used, @delegated_properties, %outer_joins, %chain_delegates, ); for my $key (keys %filters) { if (index($key,'.') != -1) { $chain_delegates{$key} = delete $filters{$key}; } } for my $co ( $class_meta, @parent_class_objects ) { my $class_name = $co->class_name; last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) ); my @id_property_objects = $co->direct_id_property_metas; if (@id_property_objects == 0) { @id_property_objects = $co->property_meta_for_name("id"); if (@id_property_objects == 0) { Carp::confess("Couldn't determine ID properties for $class_name\n"); } } my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; for my $property_name (sort keys %filters) { my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name); next unless $property; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); delete $filters{$property_name}; $pk_used = 1 if $id_properties{ $property_name }; if ($property->is_legacy_eav) { die "Old GSC EAV can be handled with a via/to/where/is_mutable=1"; } elsif ($property->is_delegated) { push @delegated_properties, $property; } elsif ($property->is_calculated || $property->is_transient) { $needs_further_boolexpr_evaluation_after_loading = 1; } else { push @sql_filters, $class_name => { $property_name => { operator => $operator, value_position => $value_position } }; } } $prev_id_column_name = $id_property_objects[0]->column_name; } # end of inheritance loop if ( my @errors = keys(%filters) ) { my $class_name = $class_meta->class_name; $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!"); Carp::confess(); } my $last_class_name = $class_name; my $last_class_object = $class_meta; my $alias_num = 1; my %joins_done; my $joins_across_data_sources; DELEGATED_PROPERTY: for my $delegated_property (@delegated_properties) { my $last_alias_for_this_chain; my $property_name = $delegated_property->property_name; my @joins = $delegated_property->_resolve_join_chain($property_name); my $relationship_name = $delegated_property->via; unless ($relationship_name) { $relationship_name = $property_name; $needs_further_boolexpr_evaluation_after_loading = 1; } my $delegate_class_meta = $delegated_property->class_meta; my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name); next unless $via_accessor_meta; my $final_accessor = $delegated_property->to; my $data_type = $via_accessor_meta->data_type; unless ($data_type) { Carp::croak "Can't resolve delegation for $property_name on class $class_name: via property $relationship_name has no data type"; } my $data_type_meta = UR::Object::Type->get($via_accessor_meta->data_type); unless ($data_type_meta) { Carp::croak "No class meta data for " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"; } my($final_accessor_meta) = $data_type_meta->_concrete_property_meta_for_class_and_name( $final_accessor ); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } # Follow the chain of via/to delegation down to where the data ultimately lives while($final_accessor_meta->is_delegated) { # May have been 'to' an id_by/id_class_by property. Stop chaining and do two queries # If we had access to the value at this point, we could continue joining through that # value's class and id next DELEGATED_PROPERTY if ($final_accessor_meta->id_by or $final_accessor_meta->id_class_by); my $prev_accessor_meta = $final_accessor_meta; $final_accessor_meta = $final_accessor_meta->to_property_meta(); unless ($final_accessor_meta) { Carp::croak("Can't resolve property '$final_accessor' of class " . $via_accessor_meta->data_type . ": Resolution involved property '" . $prev_accessor_meta->property_name . "' of class " . $prev_accessor_meta->class_name . " which is delegated, but its via/to metadata does not resolve to a known class and property"); } } $final_accessor = $final_accessor_meta->property_name; for my $join (@joins) { my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $foreign_class_name = $join->{foreign_class}; next DELEGATED_PROPERTY if ($foreign_class_name->isa('UR::Value')); my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (! $foreign_data_source) { $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } elsif ($foreign_data_source ne $ds or ! $ds->does_support_joins or ! $foreign_data_source->does_support_joins ) { push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property); next DELEGATED_PROPERTY; } my @source_property_names = @{ $join->{source_property_names} }; my @source_table_and_column_names = map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::confess("No property $_ for class $source_class_object->{class_name}\n"); } unless ($p->class_name->__meta__) { Carp::croak("Can't get class metadata for " . $p->class_name); } [$p->class_name->__meta__->class_name, $p->property_name]; } @source_property_names; my $foreign_table_name = $foreign_class_name; unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } my @foreign_property_names = @{ $join->{foreign_property_names} }; my @foreign_property_meta = map { $foreign_class_object->_concrete_property_meta_for_class_and_name($_) } @foreign_property_names; my @foreign_column_names = map { # TODO: encapsulate $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name) } @foreign_property_meta; unless (@foreign_column_names) { # all calculated properties: don't try to join any further last; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $needs_further_boolexpr_evaluation_after_loading = 1; } my $alias = $joins_done{$join->{id}}; unless ($alias) { $alias = "${relationship_name}_${alias_num}"; $alias_num++; $object_num++; push @sql_joins, "$foreign_table_name $alias" => { map { $foreign_property_names[$_] => { link_table_name => $last_alias_for_this_chain || $source_table_and_column_names[$_][0], link_column_name => $source_table_and_column_names[$_][1] } } (0..$#foreign_property_names) }; # Add all of the columns in the join table to the return list. push @all_properties, map { [$foreign_class_object, $_, $alias, $object_num] } map { $_->[1] } # These three lines are to get around a bug in perl sort { $a->[0] cmp $b->[0] } # 5.8's sort involving method calls within the sort map { [ $_->property_name, $_ ] } # sub that do sorts of their own grep { defined($_->column_name) && $_->column_name ne '' } UR::Object::Property->get( class_name => $foreign_class_name ); $joins_done{$join->{id}} = $alias; } # Set these for after all of the joins are done $last_class_name = $foreign_class_name; $last_class_object = $foreign_class_object; $last_alias_for_this_chain = $alias; } # next join unless ($delegated_property->via) { next; } my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($final_accessor); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name . " while resolving joins for property '" .$delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $needs_further_boolexpr_evaluation_after_loading = 1; next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); } # next delegated property for my $property_meta_array (@all_properties) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } #my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@all_properties); %$self = ( %$self, %$class_data, properties_for_params => \@all_properties, property_names_in_resultset_order => \@property_names_in_resultset_order, joins => \@sql_joins, rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => $rule_template->recursion_desc, recurse_property_on_this_row => $recurse_property_on_this_row, recurse_property_referencing_other_rows => $recurse_property_referencing_other_rows, recurse_resolution_by_iteration => $recurse_resolution_by_iteration, #loading_templates => $per_object_in_resultset_loading_detail, joins_across_data_sources => $joins_across_data_sources, ); return $self; } sub _init_core { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # TODO: most of this only applies to the RDBMS subclass, # but some applies to any datasource. It doesn't hurt to have the RDBMS stuff # here and ignored, but it's not placed correctly. # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_properties = @{ $class_data->{all_properties} }; # my $first_table_name = $class_data->{first_table_name}; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; my $id_property_sorter = $class_data->{id_property_sorter}; # my $order_by_clause = $class_data->{order_by_clause}; # my @lob_column_names = @{ $class_data->{lob_column_names} }; # my @lob_column_positions = @{ $class_data->{lob_column_positions} }; # my $query_config = $class_data->{query_config}; # my $post_process_results_callback = $class_data->{post_process_results_callback}; my $sub_typing_property = $class_data->{sub_typing_property}; my $class_table_name = $class_data->{class_table_name}; # individual query/boolexpr based my $recursion_desc = $rule_template->recursion_desc; my $recurse_property_on_this_row; my $recurse_property_referencing_other_rows; if ($recursion_desc) { ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc; } # _usually_ items freshly loaded from the DB don't need to be evaluated through the rule # because the SQL gets constructed in such a way that all the items returned would pass anyway. # But in certain cases (a delegated property trying to match a non-object value (which is a bug # in the caller's code from one point of view) or with calculated non-sql properties, then the # sql will return a superset of the items we're actually asking for, and the loader needs to # validate them through the rule my $needs_further_boolexpr_evaluation_after_loading; # Does fulfilling this request involve querying more than one data source? my $is_join_across_data_source; my @sql_params; my @filter_specs; my @property_names_in_resultset_order; my $object_num = 0; # 0-based, usually zero unless there are joins my @filters = $rule_template->_property_names; my %filters = map { $_ => 0 } grep { substr($_,0,1) ne '-' } @filters; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } my ( @sql_joins, @sql_filters, $prev_table_name, $prev_id_column_name, $eav_class, @eav_properties, $eav_cnt, %pcnt, $pk_used, @delegated_properties, %outer_joins, %chain_delegates, ); for my $key (keys %filters) { if (index($key,'.') != -1) { $chain_delegates{$key} = delete $filters{$key}; } } for my $co ( $class_meta, @parent_class_objects ) { # my $table_name = $co->table_name; # next unless $table_name; # $first_table_name ||= $table_name; my $class_name = $co->class_name; last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) ); my @id_property_objects = $co->direct_id_property_metas; if (@id_property_objects == 0) { @id_property_objects = $co->property_meta_for_name("id"); if (@id_property_objects == 0) { Carp::confess("Couldn't determine ID properties for $class_name\n"); } } my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; # if ($prev_table_name) # { # # die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1; # Carp::confess("No table for class $co->{class_name}") unless $table_name; # push @sql_joins, # $table_name => # { # $id_property_objects[0]->column_name => { # link_table_name => $prev_table_name, # link_column_name => $prev_id_column_name # } # }; # delete $filters{ $id_property_objects[0]->property_name } if $pk_used; # } for my $property_name (sort keys %filters) { my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name); next unless $property; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); delete $filters{$property_name}; $pk_used = 1 if $id_properties{ $property_name }; # if ($property->can("expr_sql")) { # my $expr_sql = $property->expr_sql; # push @sql_filters, # $table_name => # { # # cheap hack of putting a whitespace differentiates # # from a regular column below # " " . $expr_sql => { operator => $operator, value_position => $value_position } # }; # next; # } if ($property->is_legacy_eav) { die "Old GSC EAV can be handled with a via/to/where/is_mutable=1"; } elsif ($property->is_transient) { die "Query by transient property $property_name on $class_name cannot be done!"; } elsif ($property->is_delegated) { push @delegated_properties, $property; } elsif ($property->is_calculated) { $needs_further_boolexpr_evaluation_after_loading = 1; } else { # normal column: filter on it push @sql_filters, $class_name => { $property_name => { operator => $operator, value_position => $value_position } }; } } # $prev_table_name = $table_name; $prev_id_column_name = $id_property_objects[0]->column_name; } # end of inheritance loop if ( my @errors = keys(%filters) ) { my $class_name = $class_meta->class_name; $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!"); Carp::confess(); } my $last_class_name = $class_name; my $last_class_object = $class_meta; my $alias_num = 1; my %joins_done; my $joins_across_data_sources; DELEGATED_PROPERTY: for my $delegated_property (@delegated_properties) { my $last_alias_for_this_chain; my $property_name = $delegated_property->property_name; my @joins = $delegated_property->_resolve_join_chain($property_name); #pop @joins if $joins[-1]->{foreign_class}->isa("UR::Value"); my $relationship_name = $delegated_property->via; unless ($relationship_name) { $relationship_name = $property_name; $needs_further_boolexpr_evaluation_after_loading = 1; } my $delegate_class_meta = $delegated_property->class_meta; my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name); my $final_accessor = $delegated_property->to; my($final_accessor_meta) = $via_accessor_meta->data_type->__meta__->_concrete_property_meta_for_class_and_name( $final_accessor ); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } while($final_accessor_meta->is_delegated) { $final_accessor_meta = $final_accessor_meta->to_property_meta(); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } } $final_accessor = $final_accessor_meta->property_name; for my $join (@joins) { my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (! $foreign_data_source) { $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } elsif ($foreign_data_source ne $ds or ! $ds->does_support_joins or ! $foreign_data_source->does_support_joins ) { push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property); next DELEGATED_PROPERTY; } my @source_property_names = @{ $join->{source_property_names} }; my @source_table_and_column_names = map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::confess("No property $_ for class $source_class_object->{class_name}\n"); } [$p->class_name->__meta__->class_name, $p->property_name]; } @source_property_names; my $foreign_table_name = $foreign_class_name; unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } my @foreign_property_names = @{ $join->{foreign_property_names} }; my @foreign_property_meta = map { $foreign_class_object->_concrete_property_meta_for_class_and_name($_); } @foreign_property_names; my @foreign_column_names = map { # TODO: encapsulate $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name) } @foreign_property_meta; unless (@foreign_column_names) { # all calculated properties: don't try to join any further last; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $needs_further_boolexpr_evaluation_after_loading = 1; } my $alias = $joins_done{$join->{id}}; unless ($alias) { $alias = "${relationship_name}_${alias_num}"; $alias_num++; $object_num++; my @source_property_meta = map { $source_class_object->_concrete_property_meta_for_class_and_name($_) } @source_property_names; push @sql_joins, "$foreign_table_name $alias" => { map { my @coercion = $ds->cast_for_data_conversion( $source_property_meta[$_]->_data_type_as_class_name, $foreign_property_meta[$_]->_data_type_as_class_name, '=', 'join'); $foreign_property_names[$_] => { link_table_name => $last_alias_for_this_chain || $source_table_and_column_names[$_][0], link_column_name => $source_table_and_column_names[$_][1], left_coercion => $coercion[0], right_coercion => $coercion[1], } } (0..$#foreign_property_names) }; # Add all of the columns in the join table to the return list. push @all_properties, map { [$foreign_class_object, $_, $alias, $object_num] } map { $_->[1] } # These three lines are to get around a bug in perl sort { $a->[0] cmp $b->[0] } # 5.8's sort involving method calls within the sort map { [ $_->property_name, $_ ] } # sub that do sorts of their own grep { defined($_->column_name) && $_->column_name ne '' } UR::Object::Property->get( class_name => $foreign_class_name ); $joins_done{$join->{id}} = $alias; } # Set these for after all of the joins are done $last_class_name = $foreign_class_name; $last_class_object = $foreign_class_object; $last_alias_for_this_chain = $alias; } # next join unless ($delegated_property->via) { next; } my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($id_properties[0]); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name . " while resolving joins for property '" .$delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $needs_further_boolexpr_evaluation_after_loading = 1; next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); #push @sql_filters, # $final_table_name_with_alias => { # $sql_lvalue => { operator => $operator, value_position => $value_position } # }; } # next delegated property for my $property_meta_array (@all_properties) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } my @this_ds_properties = grep { ! $_->[1]->is_delegated and (! $_->[1]->is_calculated or $_->[1]->calculate_sql) } @all_properties; my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@this_ds_properties); %$self = ( %$self, %$class_data, properties_for_params => \@all_properties, property_names_in_resultset_order => \@property_names_in_resultset_order, joins => \@sql_joins, rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => $rule_template->recursion_desc, recurse_property_on_this_row => $recurse_property_on_this_row, recurse_property_referencing_other_rows => $recurse_property_referencing_other_rows, loading_templates => $per_object_in_resultset_loading_detail, joins_across_data_sources => $joins_across_data_sources, ); return $self; } sub _init_default { my $self = shift; my $bx_template = $self->rule_template; $self->{needs_further_boolexpr_evaluation_after_loading} = 1; my $all_possible_headers = $self->{loading_templates}[0]{property_names}; my $expected_headers; my $class_meta = $bx_template->subject_class_name->__meta__; for my $pname (@$all_possible_headers) { my $pmeta = $class_meta->property($pname); if ($pmeta->is_delegated) { next; } push @$expected_headers, $pname; } $self->{loading_templates}[0]{property_names} = $expected_headers; if ($bx_template->subject_class_name->isa('UR::Value')) { # Hack so the objects get blessed into the proper subclass in the Object Fabricator. # This is necessary so every possible UR::Value subclass doesn't need its # own "id" property defined. Without it, the data shows that these objects get # loaded as the base UR::Value class (since its "id" is defined on UR:Value) # and then would get automagically subclassed. $self->{'loading_templates'}->[0]->{'final_class_name'} = $bx_template->subject_class_name } return $self; } sub _init_remote_cache { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my $recursion_desc = $rule_template->recursion_desc; my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; my $sub_typing_property = $class_data->{'sub_typing_property'}; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } my @property_names = $class_name->__meta__->all_property_names; %$self = ( %$self, select_clause => '', select_hint => undef, from_clause => '', where_clause => '', connect_by_clause => '', order_by_clause => '', needs_further_boolexpr_evaluation_after_loading => undef, loading_templates => [], sql_params => [], filter_specs => [], property_names_in_resultset_order => \@property_names, properties_for_params => [], rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => undef, recurse_property_on_this_row => undef, recurse_property_referencing_other_rows => undef, %$class_data, ); return $self; } sub order_by_column_list { my $self = shift; $self->_resolve_order_by_and_descending_data(); return $self->{_order_by_column_list}; } sub _resolve_order_by_and_descending_data { my $self = shift; unless ($self->{_order_by_column_list}) { my %is_descending; my @order_by_columns = map { m/^-(.*)/ ? $is_descending{$1} = $1 : $_; } @{ $self->order_by_columns || [] }; $self->{_order_by_column_list} = \@order_by_columns; $self->{_order_by_column_is_descending} = \%is_descending; } } sub order_by_column_is_descending { my($self, $column_name) = @_; $self->_resolve_order_by_and_descending_data(); return $self->{_order_by_column_is_descending}->{$column_name}; } sub property_meta_for_column { my($self, $table_and_column_name) = @_; $table_and_column_name = lc($table_and_column_name); my $data_source = $self->data_source(); my ($table_name, $column_name) = $data_source->_resolve_table_and_column_from_column_name($table_and_column_name); if (my $join = $self->_get_alias_join($table_name)) { # The given $table_name was actually a join alias my $foreign_class_meta = $join->foreign_class->__meta__; my $prop_name = $foreign_class_meta->property_for_column($column_name); return $prop_name ? $foreign_class_meta->property_meta_for_name($prop_name) : undef; } else { my $class_meta = $self->class_name->__meta__; my $prop_name = $class_meta->property_for_column($table_and_column_name); return $class_meta->property_meta_for_name($prop_name); } } 1; RDBMS.pm100664023532023421 42523512544604516 16243 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::RDBMS; # NOTE:: UR::DataSource::QueryPlan has conditional logic # for this class/subclasses currently use strict; use warnings; use Scalar::Util; use List::MoreUtils; use File::Basename; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS', is => ['UR::DataSource','UR::Singleton'], is_abstract => 1, has => [ server => { is => 'Text', doc => 'the "server" part of the DBI connect string' }, login => { is => 'Text', doc => 'user name to connect as', is_optional => 1 }, auth => { is => 'Text', doc => 'authentication for the given user', is_optional => 1 }, owner => { is => 'Text', doc => 'Schema/owner name to connect to', is_optional => 1 }, ], has_optional => [ alternate_db_dsn => { is => 'Text', default_value => 0, doc => 'Set to a DBI dsn to copy all data queried from this datasource to an alternate database', }, camel_case_table_names => { is => 'Boolean', default_value => 0, doc => 'When true, dynamically calculating class names from table names will expect camel case in table names.', }, camel_case_column_names => { is => 'Boolean', default_value => 0, doc => 'When true, dynamically calculating property names from column names will expect camel case in column names.', }, _all_dbh_hashref => { is => 'HASH', len => undef, is_transient => 1 }, _last_savepoint => { is => 'Text', len => undef, is_transient => 1 }, ], valid_signals => ['query', 'query_failed', 'commit_failed', 'do_failed', 'connect_failed', 'sequence_nextval', 'sequence_nextval_failed'], doc => 'A logical DBI-based database, independent of prod/dev/testing considerations or login details.', ); # A record of objects saved to the database. It's filled in by _sync_database() # and used by the alternate DB saving code. Objects noted in this hash don't get # saved to the alternate DB my %objects_in_database_saved_by_this_process; sub database_exists { my $self = shift; warn $self->class . " failed to implement the database_exists() method. Testing connection as a surrogate. FIXME here!\n"; eval { my $c = $self->create_default_handle(); }; if ($@) { return; } return 1; } sub create_database { my $self = shift; die $self->class . " failed to implement the create_database() method!" . " Unable to initialize a new database for this data source " . $self->__display_name__ . " FIXME here.\n"; } sub _resolve_ddl_for_table { my ($self,$table, %opts) = @_; my $all = delete $opts{all}; if (%opts) { Carp::confess("odd arguments to _resolve_ddl_for_table: " . UR::Util::d(\%opts)); } my $table_name = $table->table_name; my @ddl; if ($table->{db_committed} and not $all) { my @columns = $table->columns; for my $column (@columns) { next unless $all or $column->last_object_revision eq '-'; my $column_name = $column->column_name; my $ddl = "alter table $table_name add column "; $ddl .= "\t$column_name " . $column->data_type; if ($column->data_length) { $ddl .= '(' . $column->data_length . ')'; } push(@ddl, $ddl) if $ddl; } } else { my $ddl; my @columns = $table->columns; for my $column (@columns) { next unless $all or $column->last_object_revision eq '-'; my $column_name = $column->column_name; $ddl = 'create table ' . $table_name . "(\n" unless defined $ddl; $ddl .= "\t$column_name " . $column->data_type; if ($column->data_length) { $ddl .= '(' . $column->data_length . ')'; } $ddl .= ",\n" unless $column eq $columns[-1]; } $ddl .= "\n)" if defined $ddl; push(@ddl, $ddl) if $ddl; } return @ddl; } sub generate_schema_for_class_meta { my ($self, $class_meta, $temp) = @_; # We now support on-the-fly database introspection # this gets called with the temp flag when _sync_database realizes # it knows nothing about the table in question. # We basically presume the schema is the one we would have generated # given the current class definitions # TODO: We still need to presume foreign keys are constrained. my $method = ($temp ? '__define__' : 'create'); my @defined; my $table_name = $class_meta->table_name; my @fks_to_generate; for my $p ($class_meta->parent_class_metas) { next if ($p->class_name eq 'UR::Object' or $p->class_name eq 'UR::Entity'); next unless $p->class_name->isa("UR::Object"); my @new = $self->generate_schema_for_class_meta($p,$temp); push @defined, @new; my $parent_table; if (($parent_table) = grep { $_->isa("UR::DataSource::RDBMS::Table") } @new) { my @id_by = $class_meta->id_property_names; my @column_names = map { $class_meta->property($_)->column_name } @id_by; my $r_table_name = $parent_table->table_name; ##$DB::single = 1; # get pk columns my @r_id_by = $p->id_property_names; my @r_column_names = map { $class_meta->property($_)->column_name } @r_id_by; push @fks_to_generate, [$class_meta->class_name, $table_name, $r_table_name, \@column_names, \@r_column_names]; } } my %properties_with_expected_columns = map { $_->column_name => $_ } grep { $_->column_name } $class_meta->direct_property_metas; #my %expected_constraints = # map { $_->column_name => $_ } # grep { $_->class_meta eq $class_meta } # map { $class_meta->property_meta_for_name($_) } # map { @{ $_->id_by } } # grep { $_->id_by } # $class_meta->all_property_metas; #print Data::Dumper::Dumper(\%expected_constraints); unless ($table_name) { if (my @column_names = keys %properties_with_expected_columns) { Carp::confess("class " . $class_meta->__display_name__ . " has no table_name specified for columns @column_names!"); } else { # no table, but no storable columns. all ok. return; } } ## print "handling table $table_name\n"; if ($table_name =~ /[^\w\.]/) { # pass back anything from parent classes, but do nothing for special "view" tables #$DB::single = 1; return @defined; } my $t = '-'; my $table = $self->refresh_database_metadata_for_table_name($table_name, $method); my %existing_columns; if ($table) { ## print "found table $table_name\n"; %existing_columns = map { $_->column_name => $_ } grep { $_->column_name } $table->columns; push @defined, ($table,$table->columns); } else { ## print "adding table $table_name\n"; $table = UR::DataSource::RDBMS::Table->$method( table_name => $table_name, data_source => $self->_my_data_source_id, remarks => $class_meta->doc, er_type => 'entity', last_object_revision => $t, table_type => ($table_name =~ /\s/ ? 'view' : 'table'), ); Carp::confess("Failed to create metadata or table $table_name") unless $table; push @defined, $table; } my ($update,$add,$extra) = UR::Util::intersect_lists([keys %properties_with_expected_columns],[keys %existing_columns]); for my $column_name (@$extra) { my $column = $existing_columns{$column_name}; $column->last_object_revision('?'); } for my $column_name (@$add) { my $property = $properties_with_expected_columns{$column_name}; #print "adding column $column_name\n"; my $column = UR::DataSource::RDBMS::TableColumn->$method( column_name => $column_name, table_name => $table->table_name, data_source => $table->data_source, namespace => $table->namespace, data_type => $self->object_to_db_type($property->data_type) || 'Text', data_length => $property->data_length, nullable => $property->is_optional, remarks => $property->doc, last_object_revision => $t, ); push @defined, $column; } for my $column_name (@$update) { my $property = $properties_with_expected_columns{$column_name}; my $column = $existing_columns{$column_name}; ##print "updating column $column_name with data from property " . $property->property_name . "\n"; if ($column->data_type) { $column->data_type($self->object_to_db_type($property->data_type)) if $property->data_type; } else { $column->data_type($self->object_to_db_type($property->data_type) || 'Text'); } $column->data_length($property->data_length); $column->nullable($property->is_optional); $column->remarks($property->doc); } for my $property ( $class_meta->direct_id_property_metas ) { unless (UR::DataSource::RDBMS::PkConstraintColumn->get(table_name => $table->table_name, column_name => $property->column_name, data_source => $table->data_source)) { UR::DataSource::RDBMS::PkConstraintColumn->$method( column_name => $property->column_name, data_source => $table->data_source, rank => $property->is_id, table_name => $table->table_name ); } } # this "property_metas" method filers out things which have an id_by. # it used to call ->properties, which used that method internally ...but seems like it never could have done anything? for my $property ($class_meta->property_metas) { my $id_by = $property->id_by; next unless $id_by; my $r_class_name = $property->data_type; my $r_class_meta = $r_class_name->__meta__; my $r_table_name = $r_class_meta->table_name; next unless $r_table_name; my @column_names = map { $class_meta->property($_)->column_name } @$id_by; my @r_column_names = map { $r_class_meta->property($_)->column_name } @{ $r_class_meta->id_property_names }; push @fks_to_generate, [$property->id, $table_name, $r_table_name, \@column_names, \@r_column_names ]; } for my $fk_to_generate (@fks_to_generate) { my ($fk_id, $table_name, $r_table_name, $column_names, $r_column_names) = @$fk_to_generate; my $fk = UR::DataSource::RDBMS::FkConstraint->$method( fk_constraint_name => $fk_id, table_name => $table_name, r_table_name => $r_table_name, data_source => $self->_my_data_source_id, last_object_revision => '-', ); unless ($fk) { die "failed to generate an implied foreign key constraint for $table_name => $r_table_name!" . UR::DataSource::RDBMS::FkConstraint->error_message; } push @defined, $fk; for (my $n = 0; $n < @$column_names; $n++) { my $column_name = $column_names->[$n]; my $r_column_name = $r_column_names->[$n]; my %fkcol_params = ( fk_constraint_name => $fk_id, table_name => $table_name, column_name => $column_name, r_table_name => $r_table_name, r_column_name => $r_column_name, data_source => $self->_my_data_source_id, ); my $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params); unless ($fkcol) { $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->$method(%fkcol_params); } unless ($fkcol) { die "failed to generate an implied foreign key constraint for $table_name => $r_table_name!" . UR::DataSource::RDBMS::FkConstraint->error_message; } push @defined, $fkcol; } } # handle missing meta datasource on the fly... if (@defined) { my $ns = $class_meta->namespace; my $exists = UR::Object::Type->get($ns . "::DataSource::Meta"); unless ($exists) { UR::DataSource::Meta->generate_for_namespace($ns); } } unless ($temp) { my @ddl = $self->_resolve_ddl_for_table($table); $t = $UR::Context::current->now; if (@ddl) { my $dbh = $table->data_source->get_default_handle; for my $ddl (@ddl) { $dbh->do($ddl) or Carp::confess("Failed to modify the database schema!: $ddl\n" . $dbh->errstr); for my $o ($table, $table->columns) { $o->last_object_revision($t); } } } } return @defined; } # override in architecture-oriented subclasses sub object_to_db_type { my ($self, $object_type) = @_; my $db_type = $object_type; # ... return $db_type; } # override in architecture-oriented subclasses sub db_to_object_type { my ($self, $db_type) = @_; my $object_type = $db_type; # ... return $object_type; } # FIXME - shouldn't this be a property of the class instead of a method? sub does_support_joins { 1 } sub get_class_meta_for_table { my $self = shift; my $table = shift; my $table_name = $table->table_name; return $self->get_class_meta_for_table_name($table_name); } sub get_class_meta_for_table_name { my($self,$table_name) = @_; # There is an unique constraint on classes, but only those which use # tables in an RDBMS, which dicates that there can be only two for # a given table in a given data source: one for the ghost and one # for the regular entity. We can't just fix this with a unique constraint # since classes with a null data source would be lost in some queries. my @class_meta = grep { not $_->class_name->isa("UR::Object::Ghost") } UR::Object::Type->get( table_name => $table_name, data_source => $self->class, ); unless (@class_meta) { # This will load every class in the namespace on the first execution :( ##$DB::single = 1; @class_meta = grep { not $_->class_name->isa("UR::Object::Ghost") } UR::Object::Type->get( table_name => $table_name, data_source => $self->class, ); } $self->context_return(@class_meta); } sub dbi_data_source_name { my $self = shift->_singleton_object; my $driver = $self->driver; my $server = $self->server; unless ($driver) { Carp::confess("Cannot resolve a dbi_data_source_name with an undefined driver()"); } unless ($server) { Carp::confess("Cannot resolve a dbi_data_source_name with an undefined server()"); } return 'dbi:' . $driver . ':' . $server; } *get_default_dbh = \&get_default_handle; sub get_default_handle { my $self = shift->_singleton_object; my $dbh = $self->SUPER::get_default_handle; unless ($dbh && $dbh->{Active}) { $self->__invalidate_get_default_handle__; $dbh = $self->create_default_handle(); } return $dbh; } sub get_for_dbh { my $class = shift; my $dbh = shift; my $ds_name = $dbh->{"private_UR::DataSource::RDBMS_name"}; return unless($ds_name); my $ds = UR::DataSource->get($ds_name); return $ds; } sub has_changes_in_base_context { shift->has_default_handle; # TODO: actually check, as this is fairly conservative # If used for switching contexts, we'd need to safely rollback any transactions first. } sub _dbi_connect_args { my $self = shift; my @connection; $connection[0] = $self->dbi_data_source_name; $connection[1] = $self->login; $connection[2] = $self->auth; $connection[3] = { AutoCommit => 0, RaiseError => 0 }; return @connection; } sub get_connection_debug_info { my $self = shift; my $handle_class = $self->default_handle_class; my @debug_info = ( "DBI Data Source Name: ", $self->dbi_data_source_name, "\n", "DBI Login: ", $self->login || '' , "\n", "DBI Version: ", $DBI::VERSION, "\n", "DBI Error: ", $handle_class->errstr || '(no error)', "\n", ); return @debug_info; } sub default_handle_class { 'UR::DBI' }; sub create_dbh { shift->create_default_handle_wrapper } sub create_default_handle { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } # get connection information my @connection = $self->_dbi_connect_args(); # connect my $handle_class = $self->default_handle_class; my $dbh = $handle_class->connect(@connection); unless ($dbh) { my $errstr; { no strict 'refs'; $errstr = ${"${handle_class}::errstr"}; }; my @confession = ( "Failed to connect to the database: $errstr\n", $self->get_connection_debug_info(), ); $self->__signal_observers__('connect_failed', 'connect', \@connection, $errstr); Carp::confess(@confession); } # used for reverse lookups $dbh->{'private_UR::DataSource::RDBMS_name'} = $self->class; # store the handle in a hash, since it's not a UR::Object my $all_dbh_hashref = $self->_all_dbh_hashref; unless ($all_dbh_hashref) { $all_dbh_hashref = {}; $self->_all_dbh_hashref($all_dbh_hashref); } $all_dbh_hashref->{$dbh} = $dbh; Scalar::Util::weaken($all_dbh_hashref->{$dbh}); $self->is_connected(1); return $dbh; } # The default is to ignore no tables, but derived classes # will probably override this sub _ignore_table { 0; } sub _table_name_to_use_for_metadata_objects { my($self, $schema, $table_name) = @_; return $self->owner ? $table_name : join('.', $schema, $table_name); } sub _get_table_names_from_data_dictionary { my $self = shift->_singleton_object; if (@_) { Carp::confess("get_tables does not currently take filters! FIXME."); } my $dbh = $self->get_default_handle; my $owner = $self->owner || '%'; # FIXME This will fix the immediate problem of getting classes to be created out of # views. We still need to somehow mark the resulting class as read-only my $sth = $self->get_table_details_from_data_dictionary('%', $owner, '%', 'TABLE,VIEW'); my @names; while (my $row = $sth->fetchrow_hashref) { my $table_name = $self->_table_name_to_use_for_metadata_objects(@$row{'TABLE_SCHEM','TABLE_NAME'}); $table_name =~ s/"|'//g; # Postgres puts quotes around entities that look like keywords next if $self->_ignore_table($table_name); push @names, $table_name; } return @names; } # A wrapper for DBI's table_info() since the DBD implementations of them # aren't always exactly what we need in other places in the system. Other # subclasses can override it to get custom behavior sub get_table_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('table_info',@_); } sub _get_whatever_details_from_data_dictionary { my $self = shift; my $method = shift; my $dbh = $self->get_default_handle(); return unless $dbh; return $dbh->$method(@_); } sub get_column_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('column_info',@_); } sub get_foreign_key_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('foreign_key_info',@_); } sub get_primary_key_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('primary_key_info',@_); } sub get_table_names { map { $_->table_name } shift->get_tables(@_); } sub get_tables { my $self = shift; #my $class = shift->_singleton_class_name; #return UR::DataSource::RDBMS::Table->get(data_source_id => $class); my $ds_id; if (ref $self) { if ($self->can('id')) { $ds_id = $self->id; } else { $ds_id = ref $self; } } else { $ds_id = $self; } return UR::DataSource::RDBMS::Table->get(data_source => $ds_id); } sub get_nullable_foreign_key_columns_for_table { my $self = shift; my $table = shift; my @nullable_fk_columns; my @fk = $table->fk_constraints; for my $fk (@fk){ my @fk_columns = UR::DataSource::RDBMS::FkConstraintColumn->get( fk_constraint_name => $fk->fk_constraint_name, data_source => $self->_my_data_source_id); for my $fk_col (@fk_columns){ my $column_obj = UR::DataSource::RDBMS::TableColumn->get(data_source => $self->_my_data_source_id, table_name => $fk_col->table_name, column_name=> $fk_col->column_name); unless ($column_obj) { Carp::croak("Can't find TableColumn metadata object for table name ".$fk_col->table_name." column ".$fk_col->column_name." while processing foreign key constraint named ".$fk->fk_constraint_name); } if ($column_obj->nullable and $column_obj->nullable ne 'N'){ my $col = $column_obj->column_name; push @nullable_fk_columns, $col; } } } return @nullable_fk_columns; } sub get_non_primary_key_nullable_foreign_key_columns_for_table { my $self = shift; my $table = shift; my @nullable_fk_columns = $self->get_nullable_foreign_key_columns_for_table($table); my %pk_columns = map { $_->column_name => 1} $table->primary_key_constraint_columns; my @non_pk_nullable_fk_columns; for my $fk_column (@nullable_fk_columns){ push @non_pk_nullable_fk_columns, $fk_column unless grep { $fk_column eq $_} keys %pk_columns; } return @non_pk_nullable_fk_columns; } # TODO: make "env" an optional characteristic of a class attribute # for all of the places we do this crap... sub access_level { my $self = shift; my $env = $self->_method2env("access_level"); if (@_) { if ($self->has_default_handle) { Carp::confess("Cannot change the db access level for $self while connected!"); } $ENV{$env} = lc(shift); } else { $ENV{$env} ||= "ro"; } return $ENV{$env}; } sub _method2env { my $class = shift; my $method = shift; unless ($method =~ /^(.*)::([^\:]+)$/) { $class = ref($class) if ref($class); $method = $class . "::" . $method; } $method =~ s/::/__/g; return $method; } sub resolve_class_name_for_table_name { my $self = shift->_singleton_object; my $qualified_table_name = shift; my $relation_type = shift; # Should be 'TABLE' or 'VIEW' my(undef, $table_name) = $self->_resolve_owner_and_table_from_table_name($qualified_table_name); # When a table_name conflicts with a reserved word, it ends in an underscore. $table_name =~ s/_$//; if ($self->camel_case_table_names) { $table_name = UR::Value::Text->get($table_name)->to_lemac("_"); } my $namespace = $self->get_namespace; my $vocabulary = $namespace->get_vocabulary; my @words; $vocabulary = 'UR::Vocabulary' unless eval { $vocabulary->__meta__ }; if ($vocabulary) { @words = map { $vocabulary->convert_to_title_case($_) } map { $vocabulary->plural_to_singular($_) } map { lc($_) } split("_",$table_name); } else { @words = map { ucfirst(lc($_)) } split("_",$table_name); } if ($self->can('_resolve_class_name_for_table_name_fixups')) { @words = $self->_resolve_class_name_for_table_name_fixups(@words); } my $class_name; my $addl; if ($relation_type && $relation_type =~ m/view/i) { $addl = 'View::'; } else { # Should just be for tables, temp tables, etc $addl = ''; } $class_name = $namespace . "::" . $addl . join("",@words); if (substr($class_name, -6) eq '::Type') { # Don't overwrite class metadata objects for a table called 'type' $class_name .= 'Table'; $self->warning_message("Class for table $table_name will be $class_name"); } return $class_name; } sub resolve_type_name_for_table_name { my $self = shift->_singleton_object; my $table_name = shift; if ($self->camel_case_table_names) { $table_name = UR::Value::Text->get($table_name)->to_lemac("_"); } my $namespace = $self->get_namespace; my $vocabulary = $namespace->get_vocabulary; $vocabulary = 'UR::Vocabulary' unless eval { $vocabulary->__meta__ }; my $vocab_obj = eval { $vocabulary->__meta__ }; my @words = ( ( map { $vocabulary->plural_to_singular($_) } map { lc($_) } split("_",$table_name) ) ); my $type_name = join(" ",@words); return $type_name; } sub resolve_property_name_for_column_name { my $self = shift->_singleton_object; my $column_name = shift; if ($self->camel_case_column_names) { $column_name = UR::Value::Text->get($column_name)->to_lemac("_"); } my @words = map { lc($_) } split("_",$column_name); my $type_name = join("_",@words); return $type_name; } sub _get_or_create_table_meta { my $self = shift; my ($data_source, $qualified_table_name, $db_table_name, $creation_method, $table_data, $revision_time) = @_; my $data_source_id = $self->_my_data_source_id; my $table_object = UR::DataSource::RDBMS::Table->get(data_source => $data_source_id, table_name => $qualified_table_name); if ($table_object) { # Already exists, update the existing entry # Instead of deleting and recreating the table object (the old way), # modify its attributes in-place. The name can't change but all the other # stuff might. $table_object->table_type($table_data->{TABLE_TYPE}); $table_object->data_source($data_source->class); $table_object->remarks($table_data->{REMARKS}); $table_object->last_object_revision($revision_time) if ($table_object->__changes__()); } else { # Create a brand new one from scratch $table_object = UR::DataSource::RDBMS::Table->$creation_method( table_name => $qualified_table_name, table_type => $table_data->{TABLE_TYPE}, data_source => $data_source_id, remarks => $table_data->{REMARKS}, last_object_revision => $revision_time, ); unless ($table_object) { Carp::confess("Failed to $creation_method table object for $db_table_name"); } } return $table_object; } sub refresh_database_metadata_for_table_name { my ($self,$qualified_table_name, $creation_method) = @_; $creation_method ||= 'create'; # this must be on or before the actual data dictionary queries my $revision_time = $UR::Context::current->now(); # The class definition can specify a table name as . to override the # data source's default schema/owner. my($ds_owner,$db_table_name) = $self->_resolve_owner_and_table_from_table_name($qualified_table_name); my $data_source_id = $self->_my_data_source_id; my $table_object = $self->_get_or_create_table_metadata_for_refresh($ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time); return unless $table_object; # We'll count a table object as changed even if any of the columns, # FKs, etc # were changed my $data_was_changed_for_this_table = $self->_update_column_metadata_for_refresh($ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object); if ($self->_update_foreign_key_metadata_for_refresh($ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object)) { $data_was_changed_for_this_table = 1; } if ($self->_update_primary_key_metadata_for_refresh($ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object)) { $data_was_changed_for_this_table = 1; } if ($self->_update_unique_constraint_metadata_for_refresh($ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object)) { $data_was_changed_for_this_table = 1; } $table_object->last_object_revision($revision_time) if ($data_was_changed_for_this_table); # Determine the ER type. # We have 'validation item', 'entity', and 'bridge' my $column_count = scalar($table_object->column_names) || 0; my $pk_column_count = scalar($table_object->primary_key_constraint_column_names) || 0; my $constraint_count = scalar($table_object->fk_constraint_names) || 0; if ($column_count == 1 and $pk_column_count == 1) { $table_object->er_type('validation item'); } else { if ($constraint_count == $column_count) { $table_object->er_type('bridge'); } else { $table_object->er_type('entity'); } } return $table_object; } sub _get_or_create_table_metadata_for_refresh { my($self, $ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time) = @_; my $table_sth = $self->get_table_details_from_data_dictionary('%', $ds_owner, $db_table_name, "TABLE,VIEW"); my $table_data = $table_sth->fetchrow_hashref(); unless ($table_data && %$table_data) { #$self->error_message("No data for table $table_name in data source $self."); return; } my $table_object = $self->_get_or_create_table_meta( $self, $qualified_table_name, $db_table_name, $creation_method, $table_data, $revision_time); return $table_object; } sub _update_column_metadata_for_refresh { my($self, $ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object) = @_; my $data_was_changed_for_this_table = 0; my $data_source_id = $self->_my_data_source_id; # mysql databases seem to require you to actually put in the database name in the first arg my $db_name = ($self->can('db_name')) ? $self->db_name : '%'; my $column_sth = $self->get_column_details_from_data_dictionary($db_name, $ds_owner, $db_table_name, '%'); unless ($column_sth) { $self->error_message("Error getting column data for table $db_table_name in data source $self."); return; } my $all_column_data = $column_sth->fetchall_arrayref({}); unless (@$all_column_data) { $self->error_message("No column data for table $db_table_name in data source $data_source_id"); return; } my %columns_to_delete = map {$_->column_name, $_} UR::DataSource::RDBMS::TableColumn->get( table_name => $qualified_table_name, data_source => $data_source_id); for my $column_data (@$all_column_data) { #my $id = $table_name . '.' . $column_data->{COLUMN_NAME} $column_data->{'COLUMN_NAME'} =~ s/"|'//g; # Postgres puts quotes around things that look like keywords delete $columns_to_delete{$column_data->{'COLUMN_NAME'}}; my $column_obj = UR::DataSource::RDBMS::TableColumn->get(table_name => $qualified_table_name, data_source => $data_source_id, column_name => $column_data->{'COLUMN_NAME'}); if ($column_obj) { # Already exists, change the attributes $column_obj->data_source($table_object->{data_source}); $column_obj->data_type($column_data->{TYPE_NAME}); $column_obj->nullable(substr($column_data->{IS_NULLABLE}, 0, 1)); $column_obj->data_length($column_data->{COLUMN_SIZE}); $column_obj->remarks($column_data->{REMARKS}); if ($column_obj->__changes__()) { $column_obj->last_object_revision($revision_time); $data_was_changed_for_this_table = 1; } } else { # It's new, create it from scratch $column_obj = UR::DataSource::RDBMS::TableColumn->$creation_method( column_name => $column_data->{COLUMN_NAME}, table_name => $qualified_table_name, data_source => $table_object->{data_source}, data_type => $column_data->{TYPE_NAME}, nullable => substr($column_data->{IS_NULLABLE}, 0, 1), data_length => $column_data->{COLUMN_SIZE}, remarks => $column_data->{REMARKS}, last_object_revision => $revision_time, ); $data_was_changed_for_this_table = 1; } unless ($column_obj) { Carp::confess("Failed to create a column ".$column_data->{'COLUMN_NAME'}." for table $db_table_name"); } } for my $to_delete (values %columns_to_delete) { #$self->status_message("Detected column " . $to_delete->column_name . " has gone away."); $to_delete->delete; $data_was_changed_for_this_table = 1; } return $data_was_changed_for_this_table; } sub _update_foreign_key_metadata_for_refresh { my($self, $ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object) = @_; my $data_was_changed_for_this_table = 0; my $data_source_id = $self->_my_data_source_id; # Make a note of what FKs exist in the Meta DB involving this table my @fks_in_meta_db = UR::DataSource::RDBMS::FkConstraint->get(data_source => $data_source_id, table_name => $qualified_table_name); push @fks_in_meta_db, UR::DataSource::RDBMS::FkConstraint->get(data_source => $data_source_id, r_table_name => $qualified_table_name); my %fks_in_meta_db_by_fingerprint; foreach my $fk ( @fks_in_meta_db ) { my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_meta_db_by_fingerprint{$fingerprint} = $fk; } # constraints on this table against columns in other tables my $fk_sth = $self->get_foreign_key_details_from_data_dictionary('', $ds_owner, $db_table_name, '', '', ''); my %fk; # hold the fk constraints that this invocation of foreign_key_info created my @constraints; my %fks_in_real_db; if ($fk_sth) { while (my $data = $fk_sth->fetchrow_hashref()) { foreach ( qw( FK_NAME FK_TABLE_NAME FKTABLE_NAME UK_TABLE_NAME PKTABLE_NAME FK_COLUMN_NAME FKCOLUMN_NAME UK_COLUMN_NAME PKCOLUMN_NAME ) ) { next unless defined($data->{$_}); # Postgres puts quotes around things that look like keywords $data->{$_} =~ s/"|'//g; } my $constraint_name = $data->{'FK_NAME'}; my $fk_table_name = $self->_table_name_to_use_for_metadata_objects( $data->{FK_TABLE_SCHEM} || $data->{FKTABLE_SCHEM}, $data->{'FK_TABLE_NAME'} || $data->{'FKTABLE_NAME'}); my $r_table_name = $self->_table_name_to_use_for_metadata_objects( $data->{UK_TABLE_SCHEM} || $data->{PKTABLE_SCHEM}, $data->{'UK_TABLE_NAME'} || $data->{'PKTABLE_NAME'}); my $fk_column_name = $data->{'FK_COLUMN_NAME'} || $data->{'FKCOLUMN_NAME'}; my $r_column_name = $data->{'UK_COLUMN_NAME'} || $data->{'PKCOLUMN_NAME'}; # MySQL returns primary key info with foreign_key_info()!? # They show up here with no $r_table_name or $r_column_name next unless ($r_table_name and $r_column_name); my $fk = UR::DataSource::RDBMS::FkConstraint->get(fk_constraint_name => $constraint_name, table_name => $fk_table_name, data_source => $data_source_id, r_table_name => $r_table_name ); unless ($fk) { $fk = UR::DataSource::RDBMS::FkConstraint->$creation_method( fk_constraint_name => $constraint_name, table_name => $fk_table_name, r_table_name => $r_table_name, data_source => $table_object->{data_source}, last_object_revision => $revision_time, ); $fk{$fk->id} = $fk; $data_was_changed_for_this_table = 1; } if ($fk{$fk->id}) { my %fkcol_params = ( fk_constraint_name => $constraint_name, table_name => $fk_table_name, column_name => $fk_column_name, r_table_name => $r_table_name, r_column_name => $r_column_name, data_source => $table_object->{data_source}, ); my $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params); unless ($fkcol) { $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->$creation_method(%fkcol_params); } } my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_real_db{$fingerprint} = $fk; push @constraints, $fk; } } # get foreign_key_info the other way # constraints on other tables against columns in this table my $fk_reverse_sth = $self->get_foreign_key_details_from_data_dictionary('', '', '', '', $ds_owner, $db_table_name); %fk = (); # resetting this prevents data_source referencing # tables from fouling up their fk objects if ($fk_reverse_sth) { while (my $data = $fk_reverse_sth->fetchrow_hashref()) { foreach ( qw( FK_NAME FK_TABLE_NAME FKTABLE_NAME UK_TABLE_NAME PKTABLE_NAME FK_COLUMN_NAME FKCOLUMN_NAME UK_COLUMN_NAME PKCOLUMN_NAME PKTABLE_SCHEM FKTABLE_SCHEM UK_TABLE_SCHEM FK_TABLE_SCHEM) ) { next unless defined($data->{$_}); # Postgres puts quotes around things that look like keywords $data->{$_} =~ s/"|'//g; } my $constraint_name = $data->{'FK_NAME'} || ''; my $fk_table_name = $self->_table_name_to_use_for_metadata_objects( $data->{FK_TABLE_SCHEM} || $data->{FKTABLE_SCHEM}, $data->{'FK_TABLE_NAME'} || $data->{'FKTABLE_NAME'}); my $r_table_name = $self->_table_name_to_use_for_metadata_objects( $data->{UK_TABLE_SCHEM} || $data->{PKTABLE_SCHEM}, $data->{'UK_TABLE_NAME'} || $data->{'PKTABLE_NAME'}); my $fk_column_name = $data->{'FK_COLUMN_NAME'} || $data->{'FKCOLUMN_NAME'}; my $r_column_name = $data->{'UK_COLUMN_NAME'} || $data->{'PKCOLUMN_NAME'}; # MySQL returns primary key info with foreign_key_info()?! # They show up here with no $r_table_name or $r_column_name next unless ($r_table_name and $r_column_name); my $fk = UR::DataSource::RDBMS::FkConstraint->get(fk_constraint_name => $constraint_name, table_name => $fk_table_name, r_table_name => $r_table_name, data_source => $table_object->{'data_source'}, ); unless ($fk) { $fk = UR::DataSource::RDBMS::FkConstraint->$creation_method( fk_constraint_name => $constraint_name, table_name => $fk_table_name, r_table_name => $r_table_name, data_source => $table_object->{data_source}, last_object_revision => $revision_time, ); unless ($fk) { ##$DB::single = 1; 1; } $fk{$fk->fk_constraint_name} = $fk; $data_was_changed_for_this_table = 1; } if ($fk{$fk->fk_constraint_name}) { my %fkcol_params = ( fk_constraint_name => $constraint_name, table_name => $fk_table_name, column_name => $fk_column_name, r_table_name => $r_table_name, r_column_name => $r_column_name, data_source => $table_object->{data_source}, ); unless ( UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params) ) { UR::DataSource::RDBMS::FkConstraintColumn->$creation_method(%fkcol_params); } } my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_real_db{$fingerprint} = $fk; push @constraints, $fk; } } # Find FKs still in the Meta db that don't exist in the real database anymore foreach my $fingerprint ( keys %fks_in_meta_db_by_fingerprint ) { unless ($fks_in_real_db{$fingerprint}) { my $fk = $fks_in_meta_db_by_fingerprint{$fingerprint}; my @fk_cols = $fk->get_related_column_objects(); $_->delete foreach @fk_cols; $fk->delete; } } return $data_was_changed_for_this_table; } sub _update_primary_key_metadata_for_refresh { my($self, $ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object) = @_; my $data_was_changed_for_this_table = 0; my $data_source_id = $self->_my_data_source_id; my $pk_sth = $self->get_primary_key_details_from_data_dictionary(undef, $ds_owner, $db_table_name); if ($pk_sth) { my @new_pk; while (my $data = $pk_sth->fetchrow_hashref()) { $data->{'COLUMN_NAME'} =~ s/"|'//g; # Postgres puts quotes around things that look like keywords my $pk = UR::DataSource::RDBMS::PkConstraintColumn->get( table_name => $qualified_table_name, data_source => $data_source_id, column_name => $data->{'COLUMN_NAME'}, ); if ($pk) { # Since the rank/order is pretty much all that might change, we # just delete and re-create these. # It's a no-op at save time if there are no changes. $pk->delete; } push @new_pk, [ table_name => $qualified_table_name, data_source => $data_source_id, column_name => $data->{'COLUMN_NAME'}, rank => $data->{'KEY_SEQ'} || $data->{'ORDINAL_POSITION'}, ]; } for my $data (@new_pk) { my $pk = UR::DataSource::RDBMS::PkConstraintColumn->$creation_method(@$data); unless ($pk) { $self->error_message("Failed to create primary key @$data"); return; } } } return $data_was_changed_for_this_table; } sub _update_unique_constraint_metadata_for_refresh { my($self, $ds_owner, $db_table_name, $qualified_table_name, $creation_method, $revision_time, $table_object) = @_; my $data_was_changed_for_this_table = 0; my $data_source_id = $self->_my_data_source_id; if (my $uc = $self->get_unique_index_details_from_data_dictionary($ds_owner, $db_table_name)) { my %uc = %$uc; # make a copy we can manipulate in case $uc is shared or read-only # check for redundant unique constraints # there may be both an index and a constraint for my $uc_name_1 ( keys %uc ) { my $uc_columns_1 = $uc{$uc_name_1} or next; my $uc_columns_1_serial = join ',', sort @$uc_columns_1; for my $uc_name_2 ( keys %uc ) { next if ( $uc_name_2 eq $uc_name_1 ); my $uc_columns_2 = $uc{$uc_name_2} or next; my $uc_columns_2_serial = join ',', sort @$uc_columns_2; if ( $uc_columns_2_serial eq $uc_columns_1_serial ) { delete $uc{$uc_name_1}; } } } # compare primary key constraints to unique constraints my $pk_columns_serial = join(',', sort map { $_->column_name } UR::DataSource::RDBMS::PkConstraintColumn->get( data_source => $data_source_id, table_name => $qualified_table_name, ) ); for my $uc_name ( keys %uc ) { # see if primary key constraint has the same name as # any unique constraints # FIXME - disabling this for now, the Meta DB dosen't track PK constraint names # Isn't it just as goot to check the involved columns? #if ( $table_object->primary_key_constraint_name eq $uc_name ) { # delete $uc{$uc_name}; # next; #} # see if any unique constraints cover the exact same column(s) as # the primary key column(s) my $uc_columns_serial = join ',', sort @{ $uc{$uc_name} }; if ( $pk_columns_serial eq $uc_columns_serial ) { delete $uc{$uc_name}; } } # Create new UniqueConstraintColumn objects for the columns that don't exist, and delete the # objects if they don't apply anymore foreach my $uc_name ( keys %uc ) { my %constraint_objs = map { $_->column_name => $_ } UR::DataSource::RDBMS::UniqueConstraintColumn->get( data_source => $data_source_id, table_name => $qualified_table_name, constraint_name => $uc_name, ); foreach my $col_name ( @{$uc{$uc_name}} ) { if ($constraint_objs{$col_name} ) { delete $constraint_objs{$col_name}; } else { my $uc = UR::DataSource::RDBMS::UniqueConstraintColumn->$creation_method( data_source => $data_source_id, table_name => $qualified_table_name, constraint_name => $uc_name, column_name => $col_name, ); 1; } } foreach my $obj ( values %constraint_objs ) { $obj->delete(); } } } return $data_was_changed_for_this_table; } sub _make_foreign_key_fingerprint { my($self,$fk) = @_; my @column_objects_with_name = map { [ $_->column_name, $_ ] } $fk->get_related_column_objects(); my @fk_cols = map { $_->[1] } sort {$a->[0] cmp $b->[0]} @column_objects_with_name; my $fingerprint = join(':', $fk->table_name, $fk->r_table_name, map { $_->column_name, $_->r_column_name } @fk_cols ); return $fingerprint; } sub _resolve_owner_and_table_from_table_name { my($self, $table_name) = @_; return (undef, undef) unless $table_name; if ($table_name =~ m/(\w+)\.(\w+)/) { return($1,$2); } else { return($self->owner, $table_name); } } sub _resolve_table_and_column_from_column_name { my($self, $column_name) = @_; if ($column_name =~ m/(\w+)\.(\w+)$/) { return ($1, $2); } else { return (undef, $column_name); } } # Derived classes should define a method to return a ref to an array of hash refs # describing all the bitmap indicies in the DB. Each hash ref should contain # these keys: table_name, column_name, index_name # If the DB dosen't support bitmap indicies, it should return an empty listref # This is used by the part that writes class defs based on the DB schema, and # possibly by sync_database() # Implemented methods should take one optional argument: a table name # # FIXME The API for bitmap_index and unique_index methods here aren't the same as # the other data_dictionary methods. These two return hashrefs of massaged # data while the others return DBI statement handles. sub get_bitmap_index_details_from_data_dictionary { my $class = shift; Carp::confess("Class $class didn't define its own bitmap_index_info() method"); } # Derived classes should define a method to return a ref to a hash keyed by constraint # names. Each value holds a listref of hashrefs containing these keys: # CONSTRAINT_NAME and COLUMN_NAME sub get_unique_index_details_from_data_dictionary { my $class = shift; Carp::confess("Class $class didn't define its own unique_index_info() method"); } sub _resolve_table_name_for_class_name { my($self, $class_name) = @_; for my $parent_class_name ($class_name, $class_name->inheritance) { my $parent_class = $parent_class_name->__meta__; # UR::Object::Type->get(class_name => $parent_class_name); next unless $parent_class; if (my $table_name = $parent_class->table_name) { return $table_name; } } return; } # For when there's no metaDB info for a class' table, it walks up the # ancestry of the class, and uses the ID properties to get the column # names, and assummes they must be the table primary keys. # # From there, it guesses the sequence name sub _resolve_sequence_name_from_class_id_properties { my($self, $class_name) = @_; my $class_meta = $class_name->__meta__; for my $meta ($class_meta, $class_meta->ancestry_class_metas) { next unless $meta->table_name; my @primary_keys = grep { $_ } # Only interested in the properties with columns defined map { $_->column_name } $meta->direct_id_property_metas; if (@primary_keys > 1) { Carp::croak("Tables with multiple primary keys (i.e. " . $meta->table_name . ": " . join(',',@primary_keys) . ") cannot have a surrogate key created from a sequence."); } elsif (@primary_keys == 1) { my $sequence = $self->_get_sequence_name_for_table_and_column($meta->table_name, $primary_keys[0]); return $sequence if $sequence; } } } sub _resolve_sequence_name_for_class_name { my($self, $class_name) = @_; my $table_name = $self->_resolve_table_name_for_class_name($class_name); unless ($table_name) { Carp::croak("Could not determine a table name for class $class_name"); } my $table_meta = UR::DataSource::RDBMS::Table->get( table_name => $table_name, data_source => $self->_my_data_source_id); my $sequence; if ($table_meta) { my @primary_keys = $table_meta->primary_key_constraint_column_names; if (@primary_keys == 0) { Carp::croak("No primary keys found for table " . $table_name . "\n"); } $sequence = $self->_get_sequence_name_for_table_and_column($table_name, $primary_keys[0]); } else { # No metaDB info... try and make a guess based on the class' ID properties $sequence = $self->_resolve_sequence_name_from_class_id_properties($class_name); } return $sequence; } our %sequence_for_class_name; sub autogenerate_new_object_id_for_class_name_and_rule { # The sequences in the database are named by a naming convention which allows us to connect them to the table # whose surrogate keys they fill. Look up the sequence and get a unique value from it for the object. # If and when we save, we should not get any integrity constraint violation errors. my $self = shift; my $class_name = shift; my $rule = shift; # Not used for the moment... if ($self->use_dummy_autogenerated_ids) { return $self->next_dummy_autogenerated_id; } my $sequence = $sequence_for_class_name{$class_name} || $class_name->__meta__->id_generator; my $new_id = eval { # FIXME Child classes really should use the same sequence generator as its parent # if it doesn't specify its own. # It'll be hard to distinguish the case of a class meta not explicitly mentioning its # sequence name, but there's a sequence generator in the schema for it (the current # mechanism), and when we should defer to the parent's sequence... unless ($sequence) { $sequence = $self->_resolve_sequence_name_for_class_name($class_name); if (!$sequence) { Carp::croak("No identity generator found for class " . $class_name . "\n"); } $sequence_for_class_name{$class_name} = $sequence; } $self->__signal_observers__('sequence_nextval', $sequence); $self->_get_next_value_from_sequence($sequence); }; unless (defined $new_id) { my $dbh = $self->get_default_handle; $self->__signal_observers__('sequence_nextval_failed', '', $sequence, $dbh->errstr); no warnings 'uninitialized'; Carp::croak("Can't get next value for sequence $sequence. Exception: $@. DBI error: ".$dbh->errstr); } return $new_id; } sub _get_sequence_name_for_table_and_column { my($self,$table_name,$column_name) = @_; # The default is to take the column name (should be a primary key from a table) and # change the _ID at the end of the column name with _SEQ # if column_name is all uppercase, make the sequence name end in upper case _SEQ my $replacement = $column_name eq uc($column_name) ? '_SEQ' : '_seq'; $column_name =~ s/_ID/$replacement/i; return $column_name; } sub resolve_order_by_clause { my($self, $query_plan) = @_; my $order_by_columns = $query_plan->order_by_column_list; return '' unless (@$order_by_columns); my $query_class_meta = $query_plan->class_name->__meta__; my @order_by_parts = map { my $order_by_property_meta = $query_plan->property_meta_for_column($_); unless ($order_by_property_meta) { Carp::croak("Cannot resolve property metadata for order-by column '$_' of class " . $query_class_meta->class_name); } $self->_resolve_order_by_clause_for_column($_, $query_plan, $order_by_property_meta); } @$order_by_columns; return 'order by ' . join(', ',@order_by_parts); } sub _resolve_order_by_clause_for_column { my($self, $column_name, $query_plan) = @_; return $query_plan->order_by_column_is_descending($column_name) ? $column_name . ' DESC' : $column_name; } sub do_sql { my $self = shift; my $sql = shift; my $dbh = $self->get_default_handle; my $rv = $dbh->do($sql); unless ($rv) { $self->__signal_observers__('do_failed', 'do', $sql, $dbh->errstr); Carp::croak("DBI do() failed: ".$dbh->errstr); } return $rv; } sub create_iterator_closure_for_rule { my ($self, $rule) = @_; my ($rule_template, @values) = $rule->template_and_values(); my $query_plan = $self->_resolve_query_plan($rule_template); # # the template has general class data # my $class_name = $query_plan->{class_name}; my @lob_column_names = @{ $query_plan->{lob_column_names} }; my @lob_column_positions = @{ $query_plan->{lob_column_positions} }; my $query_config = $query_plan->{query_config}; my $post_process_results_callback = $query_plan->{post_process_results_callback}; # # the template has explicit template data # my $select_clause = $query_plan->{select_clause}; my $select_hint = $query_plan->{select_hint}; my $from_clause = $query_plan->{from_clause}; my $where_clause = $query_plan->{where_clause}; my $connect_by_clause = $query_plan->{connect_by_clause}; my $group_by_clause = $query_plan->{group_by_clause}; my $sql_params = $query_plan->{sql_params}; my $filter_specs = $query_plan->{filter_specs}; my @property_names_in_resultset_order = @{ $query_plan->{property_names_in_resultset_order} }; # TODO: we get 90% of the way to a full where clause in the template, but # actually have to build it here since ther is no way to say "in (?)" and pass an arrayref :( # It _is_ possible, however, to process all of the filter specs with a constant number of params. # This would optimize the common case. my @all_sql_params = @$sql_params; for my $filter_spec (@$filter_specs) { my ($expr_sql, $operator, $value_position) = @$filter_spec; my $value = $values[$value_position]; my ($more_sql, @more_params) = $self->_extend_sql_for_column_operator_and_value($expr_sql, $operator, $value); $where_clause .= ($where_clause ? "\nand " : ($connect_by_clause ? "start with " : "where ")); if ($more_sql) { $where_clause .= $more_sql; push @all_sql_params, @more_params; } else { # error return; } } # The full SQL statement for the template, besides the filter logic, is built here. my $order_by_clause = $self->resolve_order_by_clause($query_plan); my $sql = "\nselect "; if ($select_hint) { my $hint = ''; foreach (@$select_hint) { $hint .= ' ' . $_; } $hint =~ s/\/\*\s?|\s?\*\///g; # remove embedded comment marks $sql .= "/*$hint */ "; } $sql .= $select_clause; $sql .= "\nfrom $from_clause"; $sql .= "\n$where_clause" if defined($where_clause) and length($where_clause); $sql .= "\n$connect_by_clause" if $connect_by_clause; $sql .= "\n$group_by_clause" if $group_by_clause; $sql .= "\n$order_by_clause" if $order_by_clause; $self->__signal_change__('query',$sql); my $dbh = $self->get_default_handle; my $sth = $dbh->prepare($sql,$query_plan->{query_config}); unless ($sth) { $self->__signal_observers__('query_failed', 'prepare', $sql, $dbh->errstr); $self->error_message("Failed to prepare SQL $sql\n" . $dbh->errstr . "\n"); Carp::confess($self->error_message); } unless ($sth->execute(@all_sql_params)) { $self->__signal_observers__('query_failed', 'execute', $sql, $dbh->errstr); $self->error_message("Failed to execute SQL $sql\n" . $sth->errstr . "\n" . Data::Dumper::Dumper(\@all_sql_params) . "\n"); Carp::confess($self->error_message); } die unless $sth; # FIXME - this has no effect, right? # buffers for the iterator my $next_db_row; my $pending_db_object_data; my $ur_test_fill_db = $self->alternate_db_dsn && $self->_create_sub_for_copying_to_alternate_db( $self->alternate_db_dsn, $query_plan->{loading_templates} ); my $iterator = sub { unless ($sth) { ##$DB::single = 1; return; } $next_db_row = $sth->fetchrow_arrayref; #$self->__signal_change__('fetch',$next_db_row); # FIXME: commented out because it may make fetches too slow unless ($next_db_row) { $sth->finish; $sth = undef; return; } # this handles things like BLOBS, which have a special interface to get the 'real' data if ($post_process_results_callback) { $next_db_row = $post_process_results_callback->($next_db_row); } # this is used for automated re-testing against a private database $ur_test_fill_db && $ur_test_fill_db->($next_db_row); return $next_db_row; }; # end of iterator closure Sub::Name::subname('UR::DataSource::RDBMS::__datasource_iterator(closure)__', $iterator); return $iterator; } sub _create_sub_for_copying_to_alternate_db { my($self, $connect_string, $loading_templates) = @_; my $ds_type = $self->ur_datasource_class_for_dbi_connect_string($connect_string); my $dbh = $ds_type->_create_dbh_for_alternate_db($connect_string) || do { Carp::carp("Cannot connect to alternate DB for copying: $DBI::errstr"); return sub {} }; my @saving_templates = $self->_resolve_loading_templates_for_alternate_db($loading_templates); foreach my $tmpl ( @saving_templates ) { my $class_meta = $tmpl->{data_class_name}->__meta__; $ds_type->mk_table_for_class_meta($class_meta, $dbh); } my @inserter_for_each_table = map { $self->_make_insert_closures_for_loading_template_for_alternate_db($_, $dbh) } @saving_templates; # Iterate through all the inserters, prerequisites first, for each row # returned from the database. Each inserter may return false, which means # it did not save anything to the alternate DB, for example if it # is asked to save an object with a dummy ID (< 0). In that case, no # subsequent inserters will be processed for that row return Sub::Name::subname '__altdb_inserter' => sub { foreach my $inserter ( @inserter_for_each_table ) { last unless &$inserter; } }; } sub _make_insert_closures_for_loading_template_for_alternate_db { my($self, $template, $dbh) = @_; my %seen_ids; # don't insert the same object more than once my $class_name = $template->{data_class_name}; my $class_meta = $class_name->__meta__; my $table_name = $class_meta->table_name; my $columns_string = join(', ', map { $class_meta->column_for_property($_) } @{ $template->{property_names} } ); my $insert_sql = "insert into $table_name ($columns_string) values (" . join(',', map { '?' } @{ $template->{property_names} } ) . ')'; my $insert_sth = $dbh->prepare($insert_sql) || Carp::croak("Prepare for insert on alternate DB table $table_name failed: ".$dbh->errstr); my $check_id_exists_sql = "select count(*) from $table_name where " . join(' and ', map { "$_ = ?" } map { $class_meta->column_for_property($_) } @{ $template->{id_property_names} }); my $check_id_exists_sth = $dbh->prepare($check_id_exists_sql) || Carp::croak("Prepare for check ID select on alternate DB table $table_name failed: ".$dbh->errstr); my @id_column_positions = @{$template->{id_column_positions}}; my @column_positions = @{$template->{column_positions}}; my $id_resolver = $template->{id_resolver}; my $check_id_is_not_null = _create_sub_to_check_if_id_is_not_null(@id_column_positions); my @prerequisites = $self->_make_insert_closures_for_prerequisite_tables($class_meta, $template); my $object_num = $template->{object_num}; my $inserter = Sub::Name::subname "__altdb_inserter_obj${object_num}_${class_name}" => sub { my($next_db_row) = @_; my $id = $id_resolver->(@$next_db_row[@id_column_positions]); return if _object_was_saved_to_database_by_this_process($class_name, $id); if ($check_id_is_not_null->($next_db_row) and ! $seen_ids{$id}++) { $check_id_exists_sth->execute( @$next_db_row[@id_column_positions]); my($count) = @{ $check_id_exists_sth->fetchrow_arrayref() }; unless ($count) { my @column_values = @$next_db_row[@column_positions]; $insert_sth->execute(@column_values) || Carp::croak("Inserting to alternate DB for $class_name failed"); } } return 1; }; return (@prerequisites, $inserter); } # not a method sub _create_sub_to_check_if_id_is_not_null { my(@id_columns) = @_; return sub { my $next_db_row = $_[0]; foreach my $col ( @id_columns ) { return 1 if defined $next_db_row->[$col]; } return 0; }; } my %cached_fk_data_for_table; sub _make_insert_closures_for_prerequisite_tables { my($self, $class_meta, $loading_template) = @_; $cached_fk_data_for_table{$class_meta->table_name} ||= $self->_load_fk_data_for_class_meta($class_meta); my %column_idx_for_column_name; for (my $i = 0; $i < @{ $loading_template->{property_names} }; $i++) { my $column_name = $class_meta->column_for_property( $loading_template->{property_names}->[$i] ); $column_idx_for_column_name{ $column_name } = $loading_template->{column_positions}->[$i]; } my $class_name = $class_meta->class_name; return map { $self->_make_prerequisite_insert_closure_for_fk($class_name, \%column_idx_for_column_name, $_) } @{ $cached_fk_data_for_table{ $class_meta->table_name } }; } sub _load_fk_data_for_class_meta { my($self, $class_meta) = @_; my ($db_owner, $table_name_without_owner) = $self->_resolve_owner_and_table_from_table_name($class_meta->table_name); my @fk_data; my $fk_sth = $self->get_foreign_key_details_from_data_dictionary('','','','', $db_owner, $table_name_without_owner); my %seen_fk_names; while( $fk_sth and my $row = $fk_sth->fetchrow_hashref ) { foreach my $key (qw(UK_TABLE_CAT UK_TABLE_SCHEM UK_TABLE_NAME UK_COLUMN_NAME FK_TABLE_CAT FK_TABLE_SCHEM FK_TABLE_NAME FK_COLUMN_NAME)) { no warnings 'uninitialized'; $row->{$key} =~ s/"|'//g; # Postgres puts quotes around entities that look like keywords } if (!@fk_data or $row->{ORDINAL_POSITION} == 1 or ( $row->{FK_NAME} and !$seen_fk_names{ $row->{FK_NAME} }++) ) { # part of a new FK push @fk_data, []; } push @{ $fk_data[-1] }, { %$row }; } return \@fk_data; } # return true if this list of FK columns exists for inheritance: # this table's FKs matches the given class' ID properties, and the FK points # to every ID property of the parent class sub _fk_represents_inheritance { my($load_class_name, $fk_column_list) = @_; my $load_class_meta = $load_class_name->__meta__; my %is_pk_column_for_class = map { $_ => 1 } grep { $_ } map { $load_class_meta->column_for_property($_) } $load_class_name->__meta__->id_property_names; if (scalar(@$fk_column_list) != scalar(values %is_pk_column_for_class)) { # differing number of columns vs ID properties return ''; } foreach my $fk ( @$fk_column_list ) { return '' unless $is_pk_column_for_class{ $fk->{FK_COLUMN_NAME} }; } my %checked; foreach my $parent_class_name ( $load_class_meta->inheritance ) { next if ($checked{$parent_class_name}++); my $parent_class_meta = eval { $parent_class_name->__meta__ }; next unless $parent_class_meta; # for non-ur classes my @pk_columns_for_parent = grep { $_ } map { $parent_class_meta->column_for_property($_) } $parent_class_meta->id_property_names; next if (scalar(@$fk_column_list) != scalar(@pk_columns_for_parent)); foreach my $parent_pk_column ( @pk_columns_for_parent ) { return '' unless $is_pk_column_for_class{ $parent_pk_column }; } } return 1; } sub _make_prerequisite_insert_closure_for_fk { my($self, $load_class_name, $column_idx_for_column_name, $fk_column_list) = @_; my $pk_class_name = $self->_lookup_fk_target_class_name($fk_column_list); # fks for inheritance are handled inside _resolve_loading_templates_for_alternate_db return () if _fk_represents_inheritance($load_class_name, $fk_column_list); my $pk_class_meta = $pk_class_name->__meta__; my %pk_to_fk_column_name_map = map { @$_{'UK_COLUMN_NAME','FK_COLUMN_NAME'} } @$fk_column_list; my @fk_columns = map { $column_idx_for_column_name->{$_} } map { $pk_to_fk_column_name_map{$_} } $pk_class_meta->id_property_names; if (grep { !defined } @fk_columns or !@fk_columns ) { Carp::croak(sprintf(q(Couldn't determine column order for inserting prerequisites of %s with foreign key "%s" refering to table %s with columns (%s)), $load_class_name, $fk_column_list->[0]->{FK_NAME}, $fk_column_list->[0]->{UK_TABLE_NAME}, join(', ', map { $_->{UK_COLUMN_NAME} } @$fk_column_list) )); } my $id_resolver = $pk_class_meta->get_composite_id_resolver(); my $check_id_is_not_null = _create_sub_to_check_if_id_is_not_null(@fk_columns); return Sub::Name::subname "__altdb_prereq_inserter_${pk_class_name}" => sub { my($next_db_row) = @_; if ($check_id_is_not_null->($next_db_row)) { my $id = $id_resolver->(@$next_db_row[@fk_columns]); return if _object_was_saved_to_database_by_this_process($pk_class_name, $id); # here we _do_ want to recurse back in. That way if these prerequisites # have prerequisites of their own, they'll be loaded in the recursive call. $pk_class_name->get($id); } return 1; } } # not a method sub _object_was_saved_to_database_by_this_process { my($class_name, $id) = @_; # Fast common case return 1 if exists ($objects_in_database_saved_by_this_process{$class_name}) && exists($objects_in_database_saved_by_this_process{$class_name}->{$id}); foreach my $saved_class ( keys %objects_in_database_saved_by_this_process ) { next unless ($class_name->isa($saved_class) || $saved_class->isa($class_name)); return 1 if exists($objects_in_database_saved_by_this_process{$saved_class}->{$id}); } return; } # given a UR::DataSource::RDBMS::FkConstraint, find the table this fk refers to # (the table with the pk_columns), then find which class goes with that table. sub _lookup_fk_target_class_name { my($self, $fk_column_list) = @_; my $pk_owner = $fk_column_list->[0]->{UK_TABLE_SCHEM}; my $pk_table_name = $fk_column_list->[0]->{UK_TABLE_NAME}; my $pk_table_name_with_owner = $pk_owner ? join('.', $pk_owner, $pk_table_name) : $pk_table_name; my $pk_class_name = $self->_lookup_class_for_table_name( $pk_table_name_with_owner ) || $self->_lookup_class_for_table_name( $pk_table_name ); unless ($pk_class_name) { # didn't find it. Maybe the target class isn't loaded yet # try looking up the class on the other side of the FK # and determine which property matches this FK my $fk_owner = $fk_column_list->[0]->{FK_TABLE_SCHEM}; my $fk_table_name = $fk_column_list->[0]->{FK_TABLE_NAME}; my $fk_table_name_with_owner = $fk_owner ? join('.', $fk_owner, $fk_table_name) : $fk_table_name; my $fk_class_name = $self->_lookup_class_for_table_name( $fk_table_name_with_owner ) || $self->_lookup_class_for_table_name( $fk_table_name ); if ($fk_class_name) { # get all the relation property target classes loaded my @relation_property_metas = grep { $_->id_by and $_->data_type } $fk_class_name->__meta__->properties(); foreach my $prop_meta ( @relation_property_metas ) { eval { $prop_meta->data_type->__meta__ }; } # try looking up again $pk_class_name = $self->_lookup_class_for_table_name( $pk_table_name_with_owner ) || $self->_lookup_class_for_table_name( $pk_table_name ); } } unless ($pk_class_name) { Carp::croak( sprintf(q(Couldn't determine class with table %s involved in foreign key "%s" from table %s with columns (%s)), $pk_table_name, $fk_column_list->[0]->{FK_NAME}, $fk_column_list->[0]->{FK_TABLE_NAME}, join(', ', map { $_->{FK_COLUMN_NAME} } @$fk_column_list), )); } return $pk_class_name; } # Given a query plan's loading templates, return a new list of look-alike # loading templates. This new list may look different from the original # list in the case of table inheritance: it separates out each class' table # and the columns that goes with it. sub _resolve_loading_templates_for_alternate_db { my($self, $original_loading_templates) = @_; my @loading_templates; foreach my $loading_template ( @$original_loading_templates ) { my $load_class_name = $loading_template->{data_class_name}; my %column_for_property_name; for (my $i = 0; $i < @{ $loading_template->{property_names} }; $i++) { $column_for_property_name{ $loading_template->{property_names}->[$i] } = $loading_template->{column_positions}->[$i]; } my @involved_class_metas = reverse grep { $_->table_name } $load_class_name->__meta__->all_class_metas; foreach my $class_meta ( @involved_class_metas ) { my @id_property_names = map { $_->property_name } grep { $_->column_name } $class_meta->direct_id_property_metas; my @id_column_positions = map { $column_for_property_name{$_} } @id_property_names; my @property_names = map { $_->property_name } grep { $_->column_name } $class_meta->direct_property_metas; my @column_positions = map { $column_for_property_name{$_} } @property_names; my $this_template = { id_property_names => \@id_property_names, id_column_positions => \@id_column_positions, property_names => \@property_names, column_positions => \@column_positions, table_alias => $class_meta->table_name, data_class_name => $class_meta->class_name, final_class_name => $loading_template->{final_class_name}, object_num => $loading_template->{object_num}, id_resolver => $class_meta->get_composite_id_resolver, }; push @loading_templates, $this_template } } return @loading_templates; } sub _create_dbh_for_alternate_db { my($self, $connect_string) = @_; # Support an extension of the connect string to allow user and password. # URI::DB supports these kinds of things, too. $connect_string =~ s/user=(\w+);?//; my $user = $1; $connect_string =~ s/password=(\w+);?//; my $password = $1; # Don't use $self->default_handle_class here # Generally, it'll be UR::DBI, which respects the setting for UR_DBI_NO_COMMIT. # Tests are usually run with no-commit on, and we still want to fill the # test db in that case my $handle_class = 'DBI'; $handle_class->connect($connect_string, $user || '', $password || '', { AutoCommit => 1, PrintWarn => 0 }); } # Create the table behind this class in the specified database. # used by the functionality behind the UR_TEST_FILLDB env var sub mk_table_for_class_meta { my($self, $class_meta, $dbh) = @_; return 1 unless $class_meta->has_table; $dbh ||= $self->get_default_handle; my $table_name = $class_meta->table_name(); $self->_assure_schema_exists_for_table($table_name, $dbh); # we only care about properties backed up by a real column my @props = grep { $_->column_name } $class_meta->direct_property_metas(); my $sql = "create table IF NOT EXISTS $table_name ("; my @cols; foreach my $prop ( @props ) { my $col = $prop->column_name; my $type = $self->data_source_type_for_ur_data_type($prop->data_type); my $len = $prop->data_length; my $nullable = $prop->is_optional; my $string = "$col" . " " . $type; $string .= " NOT NULL" unless $nullable; push @cols, $string; } $sql .= join(',',@cols); my @id_cols = $class_meta->direct_id_column_names(); $sql .= ", PRIMARY KEY (" . join(',',@id_cols) . ")" if (@id_cols); # Should we also check for the unique properties? $sql .= ")"; unless ($dbh->do($sql) ) { $self->error_message("Can't create table $table_name: ".$DBI::errstr."\nSQL: $sql"); return undef; } 1; } sub _assure_schema_exists_for_table { my($self, $table_name, $dbh) = @_; $dbh ||= $self->get_default_handle; my($schema_name, undef) = $self->_extract_schema_and_table_name($table_name); if ($schema_name) { $dbh->do("CREATE SCHEMA IF NOT EXISTS $schema_name") || Carp::croak("Could not create schema $schema_name: ".$dbh->errstr); } } sub _extract_schema_and_table_name { my($self, $string) = @_; my($schema_name, $table_name) = $string =~ m/(.*)\.(\w+)$/; return ($schema_name, $table_name); } sub _default_sql_like_escape_string { return '\\'; # Most RDBMSs support an 'escape' as part of a 'like' operator, except mysql } sub _format_sql_like_escape_string { my $class = shift; my $escape = shift; return "'$escape'"; } # This method is used when generating SQL for a rule template, in the joins # and also on a per-query basis to turn specific values into a where clause sub _extend_sql_for_column_operator_and_value { my($self, $expr_sql, $op, $val, $escape) = @_; my $class = $self->_sql_generation_class_for_operator($op); $escape ||= $self->_default_sql_like_escape_string; $escape = $self->_format_sql_like_escape_string($escape); return $class->generate_sql_for($expr_sql, $val, $escape); } sub _sql_generation_class_for_operator { my($self, $op) = @_; my $suffix = UR::Util::class_suffix_for_operator($op); my @classes = $self->inheritance; foreach my $class ( @classes ) { my $op_class_name = join('::', $class, 'Operator', $suffix); return $op_class_name if UR::Util::use_package_optimistically($op_class_name); } Carp::croak("Can't load SQL generation class for operator $op: $@"); } sub _value_is_null { my ($class, $value) = @_; return 1 if not defined $value; return 1 if $value eq ''; return 1 if (ref($value) eq 'HASH' and $value->{operator} eq '=' and (!defied($value->{value}) or $value->{value} eq '')); return 0; } sub _resolve_ids_from_class_name_and_sql { my $self = shift; my $class_name = shift; my $sql = shift; my $query; my @params; if (ref($sql) eq "ARRAY") { ($query, @params) = @{$sql}; } else { $query = $sql; } my $class_meta = $class_name->__meta__; my @id_columns = map { $class_meta->property_meta_for_name($_)->column_name } $class_meta->id_property_names; # query for the ids my $dbh = $self->get_default_handle(); my $sth = $dbh->prepare($query); unless ($sth) { Carp::croak("Could not prepare query $query: $DBI::errstr"); } unless ($sth->{NUM_OF_PARAMS} == scalar(@params)) { Carp::croak('The number of params supplied (' . scalar(@params) . ') does not match the number of placeholders (' . $sth->{NUM_OF_PARAMS} . ") in the supplied sql: $query"); } $sth->execute(@params); # After execute, we can see if the SQL contained all the required primary keys my @id_column_idx = map { $sth->{NAME_lc_hash}->{$_} } map { lc } @id_columns; if (grep { ! defined } @id_column_idx) { @id_columns = sort @id_columns; my @missing_ids = sort grep { ! defined($sth->{NAME_lc_hash}->{lc($_)}) } @id_columns; Carp::croak("The SQL supplied is missing one or more ID columns.\n\tExpected: " . join(', ', @id_columns) . ' but some were missing: ' . join(', ', @missing_ids) . " for query: $query"); } my $id_resolver = $class_name->__meta__->get_composite_id_resolver(); my $id_values = $sth->fetchall_arrayref(\@id_column_idx); return [ map { $id_resolver->(@$_) } @$id_values ]; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $changed_objects = delete $params{changed_objects}; my %objects_by_class_name; for my $obj (@$changed_objects) { my $class_name = ref($obj); $objects_by_class_name{$class_name} ||= []; push @{ $objects_by_class_name{$class_name} }, $obj; if ($self->alternate_db_dsn) { $objects_in_database_saved_by_this_process{$class_name}->{$obj->id} = 1; } } my $dbh = $self->get_default_handle; # # Determine what commands need to be executed on the database # to sync those changes, and categorize them by type and table. # # As we iterate through changes, keep track of all of the involved tables. my %all_tables; # $all_tables{$table_name} = $number_of_commands; # Make a hash for each type of command keyed by table name. my %insert; # $insert{$table_name} = [ $change1, $change2, ...]; my %update; # $update{$table_name} = [ $change1, $change2, ...]; my %delete; # $delete{$table_name} = [ $change1, $change2, ...]; # Make a master hash referencing each of the above. # $explicit_commands_by_type_and_table{'insert'}{$table} = [ $change1, $change2 ...] my %explicit_commands_by_type_and_table = ( 'insert' => \%insert, 'update' => \%update, 'delete' => \%delete ); # Build the above data structures. { no warnings; for my $class_name (sort keys %objects_by_class_name) { for my $obj (@{ $objects_by_class_name{$class_name} }) { my @commands = $self->_default_save_sql_for_object($obj); next unless @commands; for my $change (@commands) { #$commands{$change} = $change; # Example change: # { type => 'update', table_name => $table_name, # column_names => \@changed_cols, sql => $sql, # params => \@values, class => $table_class, id => $id }; # There are often multiple changes per object, espeically # when the object is spread across multiple tables because of # inheritance. We classify each change by the table and # the class immediately associated with the table, even if # the class in an abstract parent class on the object. my $table_name = $change->{table_name}; my $id = $change->{id}; $all_tables{$table_name}++; if ($change->{type} eq 'insert') { push @{ $insert{$table_name} }, $change; } elsif ($change->{type} eq 'update') { push @{ $update{$table_name} }, $change; } elsif ($change->{type} eq 'delete') { push @{ $delete{$table_name} }, $change; } else { print "UNKNOWN COMMAND TYPE $change->{type} $change->{sql}\n"; } } } } } # Determine which tables require a lock; my %tables_requiring_lock; for my $table_name (keys %all_tables) { my $table_object = $self->_get_table_object($table_name); unless ($table_object) { warn "looking up schema for RDBMS table $table_name...\n"; $table_object = $self->refresh_database_metadata_for_table_name($table_name); unless ($table_object) { die "Failed to generate table data for $table_name!"; } } if (my @bitmap_index_names = $table_object->bitmap_index_names) { my $changes; if ($changes = $insert{$table_name} or $changes = $delete{$table_name}) { $tables_requiring_lock{$table_name} = 1; } elsif (not $tables_requiring_lock{$table_name}) { $changes = $update{$table_name}; my @column_names = sort map { @{ $_->{column_names} } } @$changes; my $last_column_name = ""; for my $column_name (@column_names) { next if $column_name eq $last_column_name; my $column_obj = UR::DataSource::RDBMS::TableColumn->get( data_source => $table_object->data_source, table_name => $table_name, column_name => $column_name, ); if ($column_obj->bitmap_index_names) { $tables_requiring_lock{$table_name} = 1; last; } $last_column_name = $column_name; } } } } # # Make a mapping of prerequisites for each command, # and a reverse mapping of dependants for each command. # my %all_table_commands; my %prerequisites; my %dependants; for my $table_name (keys %all_tables) { my $table = $self->_get_table_object($table_name); my @fk = $table->fk_constraints; my $matched_table_name; if ($insert{$table_name}) { $matched_table_name = 1; $all_table_commands{"insert $table_name"} = 1; } if ($update{$table_name}) { $matched_table_name = 1; $all_table_commands{"update $table_name"} = 1; } if ($delete{$table_name}) { $matched_table_name = 1; $all_table_commands{"delete $table_name"} = 1; } unless ($matched_table_name) { Carp::carp("Possible metadata inconsistency: A change on table $table_name was not an insert, update or delete!"); } my $tmparray; # handle multiple differnt ops on the same table if ($insert{$table_name} and $update{$table_name}) { # insert before update $tmparray = $prerequisites{"update $table_name"}{"insert $table_name"} ||= []; $tmparray = $dependants{"insert $table_name"}{"update $table_name"} ||= []; } if ($delete{$table_name} and $update{$table_name}) { # update before delete $tmparray = $prerequisites{"delete $table_name"}{"update $table_name"} ||= []; $tmparray = $dependants{"update $table_name"}{"delete $table_name"} ||= []; } if ($delete{$table_name} and $insert{$table_name} and not $update{$table_name}) { # delete before insert $tmparray = $prerequisites{"insert $table_name"}{"delete $table_name"} ||= []; $tmparray = $dependants{"delete $table_name"}{"insert $table_name"} ||= []; } # Go through the constraints. for my $fk (@fk) { my $r_table_name = $fk->r_table_name; # RULES: # insert r_table_name before insert table_name # insert r_table_name before update table_name # delete table_name before delete r_table_name # update table_name before delete r_table_name if ($insert{$table_name} and $insert{$r_table_name}) { $tmparray = $prerequisites{"insert $table_name"}{"insert $r_table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"insert $r_table_name"}{"insert $table_name"} ||= []; push @$tmparray, $fk; } if ($update{$table_name} and $insert{$r_table_name}) { $tmparray = $prerequisites{"update $table_name"}{"insert $r_table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"insert $r_table_name"}{"update $table_name"} ||= []; push @$tmparray, $fk; } if ($delete{$r_table_name} and $delete{$table_name}) { $tmparray = $prerequisites{"delete $r_table_name"}{"delete $table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"delete $table_name"}{"delete $r_table_name"} ||= []; push @$tmparray, $fk; } if ($delete{$r_table_name} and $update{$table_name}) { $tmparray = $prerequisites{"delete $r_table_name"}{"update $table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"update $table_name"}{"delete $r_table_name"} ||= []; push @$tmparray, $fk; } } } # # Use the above mapping to build an ordered list of general commands. # Note that the general command is something like "insert EMPLOYEES", # while the explicit command is an exact insert statement with params. # my @general_commands_in_order; my %self_referencing_table_commands; my %all_unresolved = %all_table_commands; my $unresolved_count; my $last_unresolved_count = 0; my @ready_to_add = (); while ($unresolved_count = scalar(keys(%all_unresolved))) { if ($unresolved_count == $last_unresolved_count) { # We accomplished nothing on the last iteration. # We are in an infinite loop unless something is done. # Rather than die with an error, issue a warning and attempt to # brute-force the sync. # Process something with minimal deps as a work-around. my @ordered_by_least_number_of_prerequisites = sort{ scalar(keys(%{$prerequisites{$a}})) <=> scalar(keys(%{$prerequisites{$b}})) } grep { $prerequisites{$_} } keys %all_unresolved; @ready_to_add = ($ordered_by_least_number_of_prerequisites[0]); warn "Circular dependency! Pushing @ready_to_add to brute-force the save.\n"; #print STDERR Data::Dumper::Dumper(\%objects_by_class_name, \%prerequisites, \%dependants ) . "\n"; } else { # This is the normal case. It is either the first iteration, # or we are on additional iterations with some progress made # in the last iteration. # Find commands which have no unresolved prerequisites. @ready_to_add = grep { not $prerequisites{$_} } keys %all_unresolved; # If there are none of the above, find commands # with only self-referencing prerequisites. unless (@ready_to_add) { # Find commands with only circular dependancies. @ready_to_add = # The circular prerequisite must be the only prerequisite on the table. grep { scalar(keys(%{$prerequisites{$_}})) == 1 } # The prerequisite must be the same as the the table itself. grep { $prerequisites{$_}{$_} } # There must be prerequisites for the given table, grep { $prerequisites{$_} } # Look at all of the unresolved table commands. keys %all_unresolved; # Note this for below. # It records the $fk object which is circular. for my $table_command (@ready_to_add) { $self_referencing_table_commands{$table_command} = $prerequisites{$table_command}{$table_command}; } } } # Record our current unresolved count for comparison on the next iteration. $last_unresolved_count = $unresolved_count; for my $db_command (@ready_to_add) { # Put it in the list. push @general_commands_in_order, $db_command; # Delete it from the main hash of command/table pairs # for which dependencies are not resolved. delete $all_unresolved{$db_command}; # Find anything which depended on this command occurring first # and remove this command from that command's prerequisite list. for my $dependant (keys %{ $dependants{$db_command} }) { # Tell it to take us out of its list of prerequisites. delete $prerequisites{$dependant}{$db_command} if $prerequisites{$dependant}; # Get rid of the prereq entry if it is empty; delete $prerequisites{$dependant} if (keys(%{ $prerequisites{$dependant} }) == 0); } # Note that nothing depends on this command any more since it has been queued. delete $dependants{$db_command}; } } # Go through the ordered list of general commands (ie "insert TABLE_NAME") # and build the list of explicit commands. my @explicit_commands_in_order; for my $general_command (@general_commands_in_order) { my ($dml_type,$table_name) = split(/\s+/,$general_command); if (my $circular_fk_list = $self_referencing_table_commands{$general_command}) { # A circular foreign key requires that the # items be inserted in a specific order. my (@rcol_sets) = map { [ $_->column_names ] } @$circular_fk_list; # Get the IDs and objects which need to be saved. my @cmds = @{ $explicit_commands_by_type_and_table{$dml_type}{$table_name} }; my @ids = map { $_->{id} } @cmds; # my @objs = $cmds[0]->{class}->is_loaded(\@ids); my $is_loaded_class = ($dml_type eq 'delete') ? $cmds[0]->{class}->ghost_class : $cmds[0]->{class}; my @objs = $is_loaded_class->is_loaded(\@ids); my %objs = map { $_->id => $_ } @objs; # Produce the explicit command list in dep order. my %unsorted_cmds = map { $_->{id} => $_ } @cmds; my $add; my @local_explicit_commands; my %adding; $add = sub { my ($cmd) = @_; if ($adding{$cmd}) { ##$DB::single = 1; Carp::confess("Circular foreign key!") unless $main::skip_croak; } $adding{$cmd} = 1; my $obj = $objs{$cmd->{id}}; my $class_meta = $obj->class->__meta__; for my $rcol_set (@rcol_sets) { my @ordered_values = map { $obj->$_ } map { $class_meta->property_for_column($_) } @$rcol_set; my $pid = $obj->class->__meta__->resolve_composite_id_from_ordered_values(@ordered_values); if (defined $pid) { # This recursive foreign key dep may have been optional my $pcmd = delete $unsorted_cmds{$pid}; $add->($pcmd) if $pcmd; } } delete $adding{$cmd}; push @local_explicit_commands, $cmd; }; for my $cmd (@cmds) { next unless $unsorted_cmds{$cmd->{id}}; $add->(delete $unsorted_cmds{$cmd->{id}}); } if ($dml_type eq 'delete') { @local_explicit_commands = reverse @local_explicit_commands; } push @explicit_commands_in_order, @local_explicit_commands; } else { # Order is irrelevant on non-self-referencing tables. push @explicit_commands_in_order, @{ $explicit_commands_by_type_and_table{$dml_type}{$table_name} }; } } my %table_objects_by_class_name; my %column_objects_by_class_and_column_name; # Make statement handles. my %sth; for my $cmd (@explicit_commands_in_order) { my $sql = $cmd->{sql}; unless ($sth{$sql}) { my $class_name = $cmd->{class}; # get the db handle to use for this class my $dbh = $cmd->{dbh}; my $sth = $dbh->prepare($sql); $sth{$sql} = $sth; unless ($sth) { $self->__signal_observers__('commit_failed', 'prepare', $sql, $dbh->errstr); $self->error_message("Error preparing SQL:\n$sql\n" . $dbh->errstr . "\n"); return; } my $tables = $table_objects_by_class_name{$class_name}; my $class_object = $class_name->__meta__; unless ($tables) { my $tables; my @all_table_names = $class_object->all_table_names; for my $table_name (@all_table_names) { my $table = $self->_get_table_object($table_name); push @$tables, $table; $column_objects_by_class_and_column_name{$class_name} ||= {}; my $columns = $column_objects_by_class_and_column_name{$class_name}; unless (%$columns) { for my $column ($table->columns) { $columns->{$column->column_name} = $column; } } } $table_objects_by_class_name{$class_name} = $tables; } my @column_objects; foreach my $column_name ( @{ $cmd->{column_names} } ) { my $column = $column_objects_by_class_and_column_name{$class_name}->{$column_name}; unless ($column) { FIND_IN_ANCESTRY: for my $ancestor_class_name ($class_object->ancestry_class_names) { $column = $column_objects_by_class_and_column_name{$ancestor_class_name}->{$column_name}; if ($column) { $column_objects_by_class_and_column_name{$class_name}->{$column_name} = $column; last FIND_IN_ANCESTRY; } } } # If we didn't find a column object, then $column will be undef # and we'll have to guess what it looks like push @column_objects, $column; } # print "Column Types: @column_types\n"; $self->_alter_sth_for_selecting_blob_columns($sth,\@column_objects); } } # DBI docs say that if AutoCommit is on, then starting a transaction will temporarily # turn it off. When the handle gets commit() or rollback(), it will get turned back # on automatically by DBI if ($dbh->{AutoCommit} and ! eval { $dbh->begin_work; 1 } ) { Carp::croak(sprintf('Cannot begin transaction on data source %s: %s', $self->id, $dbh->errstr)); } # Set a savepoint if possible. my $savepoint; if ($self->can_savepoint) { $savepoint = $self->_last_savepoint; if ($savepoint) { $savepoint++; } else { $savepoint=1; } my $sp_name = "sp".$savepoint; unless ($self->set_savepoint($sp_name)) { $self->error_message("Failed to set a savepoint on " . $self->class . ": " . $dbh->errstr ); return; } $self->_last_savepoint($savepoint); } # Do any explicit table locking necessary. if (my @tables_requiring_lock = sort keys %tables_requiring_lock) { $self->debug_message("Locking tables: @tables_requiring_lock."); my $max_failed_attempts = 10; for my $table_name (@tables_requiring_lock) { my $table = $self->_get_table_object($table_name); my $dbh = $table->dbh; my $sth = $dbh->prepare("lock table $table_name in exclusive mode"); my $failed_attempts = 0; my @err; for (1) { unless ($sth->execute) { $failed_attempts++; $self->warning_message( "Failed to lock $table_name (attempt # $failed_attempts): " . $sth->errstr ); push @err, $sth->errstr; unless ($failed_attempts >= $max_failed_attempts) { redo; } } } if ($failed_attempts > 1) { my $err = join("\n",@err); if ($failed_attempts >= $max_failed_attempts) { $self->error_message( "Could not obtain an exclusive table lock on table " . $table_name . " after $failed_attempts attempts" ); $self->rollback_to_savepoint($savepoint); return; } } } } # Execute the commands in the correct order. my @failures; my $last_failure_count = 0; my @previous_failure_sets; # If there are failures, we fall-back to brute force and send # a message to support to debug the inefficiency. my $skip_fault_tolerance_check = 1; for (1) { @failures = (); for my $cmd (@explicit_commands_in_order) { unless ($sth{$cmd->{sql}}->execute(@{$cmd->{params}})) { my $dbh = $cmd->{dbh}; # my $dbh = UR::Context->resolve_data_source_for_object($cmd->{class})->get_default_handle; $self->__signal_observers__('commit_failed', 'execute', $cmd->{sql}, $dbh->errstr); push @failures, {cmd => $cmd, error_message => $sth{$cmd->{sql}}->errstr}; last if $skip_fault_tolerance_check; } $sth{$cmd->{sql}}->finish(); } if (@failures) { # There have been some failures. In case the error has to do with # a failure to correctly determine dependencies in the code above, # we will retry the set of failed commands. This repeats as long # as some progress is made on each iteration. if ( (@failures == $last_failure_count) or $skip_fault_tolerance_check) { # We've tried this exact set of comands before and failed. # This is a real error. Stop retrying and report. for my $error (@failures) { $self->error_message($self->id . ": Error executing SQL:\n$error->{cmd}{sql}\n" . "PARAMS: " . join(', ',map { defined($_) ? "'$_'" : '(undef)' } @{$error->{cmd}{params}}) . "\n" . $error->{error_message} . "\n"); } last; } else { # We've failed, but we haven't retried this exact set of commands # and found the exact same failures. This is either the first failure, # or we had failures before and had success on the last brute-force # approach to sorting commands. Try again. push @previous_failure_sets, \@failures; @explicit_commands_in_order = map { $_->{cmd} } @failures; $last_failure_count = scalar(@failures); $self->warning_message("RETRYING SAVE"); redo; } } } # Rollback to savepoint if there are errors. if (@failures) { if (!$savepoint or $savepoint eq "NONE") { # A failure on a database which does not support savepoints. # We must rollback the entire transacation. # This is only a problem for a mixed raw-sql and UR::Object environment. $dbh->rollback; } else { $self->_reverse_sync_database(); } # Return false, indicating failure. return; } unless ($self->_set_specified_objects_saved_uncommitted($changed_objects)) { Carp::confess("Error setting objects to a saved state after sync_database. Exiting."); return; } if (exists $params{'commit_on_success'} and ($params{'commit_on_success'} eq '1')) { # Commit the current transaction. # The handles will automatically update their objects to # a committed state from the one set above. # It will throw an exception on failure. $dbh->commit; } # Though we succeeded, see if we had to use the fault-tolerance code to # do so, and warn software support. This should never occur. if (@previous_failure_sets) { my $msg = "Dependency failure saving: " . Dumper(\@explicit_commands_in_order) . "\n\nThe following error sets were produced:\n" . Dumper(\@previous_failure_sets) . "\n\n" . Carp::cluck() . "\n\n"; $self->warning_message($msg); $UR::Context::current->send_email( To => UR::Context::Process->support_email, Subject => 'sync_database dependency sort failure', Message => $msg ) or $self->warning_message("Failed to send error email!"); } return 1; } # this is necessary for overriding data source names when looking up table metadata with # bifurcated oracle/postgres syncs in testing. sub _my_data_source_id { my $self = shift; return ref($self) ? $self->id : $self; } sub _get_table_object { my($self, $ds_table) = @_; my $data_source_id = $self->_my_data_source_id; my $table = UR::DataSource::RDBMS::Table->get( table_name => $ds_table, data_source => $data_source_id) || UR::DataSource::RDBMS::Table->get( table_name => $ds_table, data_source => 'UR::DataSource::Meta'); return $table; } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; return; } sub _reverse_sync_database { my $self = shift; unless ($self->can_savepoint) { # This will not respect manual DML # Developers must not use this back door on non-savepoint databases. $self->get_default_handle->rollback; return "NONE"; } my $savepoint = $self->_last_savepoint; unless ($savepoint) { Carp::confess("No savepoint set!"); } my $sp_name = "sp".$savepoint; unless ($self->rollback_to_savepoint($sp_name)) { $self->error_message("Error removing savepoint $savepoint " . $self->get_default_handle->errstr); return 1; } $self->_last_savepoint(undef); return $savepoint; } # Given a table object and a list of primary key values, return # a where clause to match a row. Some values may be undef (NULL) # and it properly writes "column IS NULL". As a side effect, the # @$values list is altered to remove the undef value sub _matching_where_clause { my($self,$table_obj,$values) = @_; unless ($table_obj) { Carp::confess("No table passed to _matching_where_clause for $self!"); } my @pks = $table_obj->primary_key_constraint_column_names; my @where; # in @$values, the updated data values always seem to be before the where clause # values but still in the right order, so start at the right place my $skip = scalar(@$values) - scalar(@pks); for (my($pk_idx,$values_idx) = (0,$skip); $pk_idx < @pks;) { if (defined $values->[$values_idx]) { push(@where, $pks[$pk_idx] . ' = ?'); $pk_idx++; $values_idx++; } else { push(@where, $pks[$pk_idx] . ' IS NULL'); splice(@$values, $values_idx, 1); $pk_idx++; } } return join(' and ', @where); } sub _id_values_for_primary_key { my ($self,$table_obj,$object_to_save) = @_; unless ($table_obj && $object_to_save) { Carp::confess("Both table and object_to_save should be passed for $self!"); } my $class_obj; # = $object_to_save->__meta__; foreach my $possible_class_obj ($object_to_save->__meta__->all_class_metas) { next unless ($possible_class_obj->table_name); if ( $possible_class_obj->table_name eq $table_obj->table_name ) { $class_obj = $possible_class_obj; last; } } unless (defined $class_obj) { Carp::croak("Can't find class object with table " . $table_obj->table_name . " while searching inheritance for object of class ".$self->class); } my @pk_cols = $table_obj->primary_key_constraint_column_names; my %pk_cols = map { $_ => 1 } @pk_cols; # this previously went to $object_to_save->__meta__, which is nearly the same thing but not quite my @values = $class_obj->resolve_ordered_values_from_composite_id($object_to_save->id); my @columns = $class_obj->direct_id_column_names; foreach my $col_in_class ( @columns ) { unless ($pk_cols{$col_in_class}) { my $table_name = $table_obj->table_name; my $class_name = $class_obj->class_name; Carp::croak("While committing, metadata for table $table_name does not match class $class_name.\n Table primary key columns are " . join(', ',@pk_cols) . "\n class ID property columns " . join(', ', @columns)); } } my $i=0; my %column_index = map { $_ => $i++ } @columns; my @bad_pk_cols = grep { ! exists($column_index{$_}) } @pk_cols; if (@bad_pk_cols) { my $table_name = $table_obj->table_name; Carp::croak("Metadata for table $table_name is inconsistent with class ".$class_obj->class_name.".\n" . "Column(s) named " . join(',',@bad_pk_cols) . " appear as primary key constraint columns, " . "but do not appear as ID column names. Check the dd_pk_constraint_columns data in the " . "MetaDB and the ID properties of the class definition"); } my @id_values_in_pk_order = @values[@column_index{@pk_cols}]; return @id_values_in_pk_order; } sub _lookup_class_for_table_name { my $self = shift; my $table_name = shift; my @table_class_obj = grep { $_->class_name !~ /::Ghost$/ } UR::Object::Type->is_loaded(data_source_id => $self->id, table_name => $table_name); # Like _get_table_object, we need to look in the data source and if the # object wasn't found then in 'UR::DataSource::Meta' in order to mimic # behavior elsewhere. unless (@table_class_obj) { @table_class_obj = grep { $_->class_name !~ /::Ghost$/ } UR::Object::Type->is_loaded(data_source_id => 'UR::DataSource::Meta', table_name => $table_name); } my $table_class; my $table_class_obj; if (@table_class_obj == 1) { $table_class_obj = $table_class_obj[0]; return $table_class_obj->class_name; } elsif (@table_class_obj > 1) { Carp::confess("Got more than one class object for $table_name, this should not happen: @table_class_obj"); } } sub _default_save_sql_for_object { my $self = shift; my $object_to_save = shift; my %params = @_; my ($class,$id) = ($object_to_save->class, $object_to_save->id); my $class_object = $object_to_save->__meta__; # This object may have uncommitted changes already saved. # If so, work from the last saved data. # Normally, we go with the last committed data. my $compare_version = ($object_to_save->{'db_saved_uncommitted'} ? 'db_saved_uncommitted' : 'db_committed'); # Determine what the overall save action for the object is, # and get a specific change summary if we're doing an update. my ($action,$change_summary); if ($object_to_save->isa('UR::Object::Ghost')) { $action = 'delete'; } elsif ($object_to_save->{$compare_version}) { $action = 'update'; $change_summary = $object_to_save->property_diff($object_to_save->{$compare_version}); } else { $action = 'insert'; } # Handle each table. There is usually only one, unless, # there is inheritance within the schema. my @save_table_names = grep { not /[^\w\.]/ } # remove any views from the list List::MoreUtils::uniq($class_object->all_table_names); @save_table_names = reverse @save_table_names unless ($object_to_save->isa('UR::Entity::Ghost')); my @commands; for my $table_name (@save_table_names) { # Get general info on the table we're working-with. my $dsn = ref($self) ? $self->_my_data_source_id: $self; # The data source name my $table = $self->_get_table_object($table_name); unless ($table) { $self->generate_schema_for_class_meta($class_object,1); # try again... $table = $self->_get_table_object($table_name); unless ($table) { Carp::croak("No table $table_name found for data source $dsn"); } } my $table_class = $self->_lookup_class_for_table_name($table_name); if (!$table_class) { Carp::croak("NO CLASS FOR $table_name\n"); } my $data_source = $UR::Context::current->resolve_data_source_for_object($object_to_save); unless ($data_source) { Carp::croak("Couldn't resolve data source for object ".$object_to_save->__display_name__.":\n" . Data::Dumper::Dumper($object_to_save)); } # The "action" now can vary on a per-table basis. my $table_action = $action; # Handle re-classification of objects. # We skip deletion and turn insert into update in these cases. if ( ($table_class ne $class) and ( ($table_class . "::Ghost") ne $class) ) { if ($action eq 'delete') { # see if the object we're deleting actually exists reclassified my $replacement = $table_class->is_loaded($id); if ($replacement) { next; } } elsif ($action eq 'insert') { # see if the object we're inserting is actually a reclassification # of a pre-existing object my $replacing = $table_class->ghost_class->is_loaded($id); if ($replacing) { $table_action = 'update'; $change_summary = $object_to_save->property_diff(%$replacing); } } } # Determine the $sql and @values needed to save this object. if ($table_action eq 'delete') { # A row loaded from the database with its object deleted. # Delete the row in the database. #grab fk_constraints so we can undef non primary-key nullable fks before delete my @non_pk_nullable_fk_columns = $self->get_non_primary_key_nullable_foreign_key_columns_for_table($table); my @values = $self->_id_values_for_primary_key($table,$object_to_save); my $where = $self->_matching_where_clause($table, \@values); if (@non_pk_nullable_fk_columns) { #generate an update statement to set nullable fk columns to null pre delete my $update_sql = "UPDATE "; $update_sql .= "$table_name SET "; $update_sql .= join(", ", map { "$_=?"} @non_pk_nullable_fk_columns); $update_sql .= " WHERE $where"; my @update_values = @values; for (@non_pk_nullable_fk_columns){ unshift @update_values, undef; } my $update_command = { type => 'update', table_name => $table_name, column_names => \@non_pk_nullable_fk_columns, sql => $update_sql, params => \@update_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; push @commands, $update_command; } my $sql = " DELETE FROM "; $sql .= "$table_name WHERE $where"; push @commands, { type => 'delete', table_name => $table_name, column_names => undef, sql => $sql, params => \@values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; #print Data::Dumper::Dumper \@commands; } elsif ($table_action eq 'update') { # Pre-existing row. # Update in the database if there are columns which have changed. my $changes_for_this_table; if (@save_table_names > 1) { my @changes = map { $_ => $change_summary->{$_} } grep { $class_object->table_for_property($_) eq $table_name } keys %$change_summary; $changes_for_this_table = {@changes}; } else { # Shortcut and use the overall changes summary when # there is only one table. $changes_for_this_table = $change_summary; } my(@changed_cols,@values); for my $property (keys %$changes_for_this_table) { my $column_name = $class_object->column_for_property($property); Carp::croak("No column in table $table_name for property $property?") unless $column_name; push @changed_cols, $column_name; push @values, $changes_for_this_table->{$property}; } if (@changed_cols) { my @changed_values = map { defined ($_) && $object_to_save->can($_) ? $object_to_save->$_ : undef } map { $class_object->property_for_column($_) || undef } @changed_cols; my @id_values = $self->_id_values_for_primary_key($table,$object_to_save); if (scalar(@changed_cols) != scalar(@changed_values)) { no warnings 'uninitialized'; my $mapping = join("\n", map { " $_ => ".$class_object->property_for_column($_) } @changed_cols); Carp::croak("Column count mismatch while updating table $table_name. " . "The table metadata expects to see ".scalar(@changed_cols) . " columns, but ".scalar(@values)." were retrieved from the object of type " . $object_to_save->class . ".\nCurrent column => property mapping:\n$mapping\n" . "There is probably a mismatch between the database column metadata and the column_name " . "property metadata"); } my @all_values = ( @changed_values, @id_values ); my $where = $self->_matching_where_clause($table, \@all_values); my $sql = " UPDATE "; $sql .= "$table_name SET " . join(",", map { "$_ = ?" } @changed_cols) . " WHERE $where"; push @commands, { type => 'update', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@all_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } elsif ($table_action eq 'insert') { # An object without a row in the database. # Insert into the database. my @changed_cols = reverse sort map { $class_object->column_for_property($_->property_name) } grep { ! $_->is_transient } grep { ($class_object->table_for_property($_->property_name) || '') eq $table_name } grep { $_->column_name } List::MoreUtils::uniq($class_object->all_property_metas()); my $sql = " INSERT INTO "; $sql .= "$table_name (" . join(",", @changed_cols) . ") VALUES (" . join(',', split(//,'?' x scalar(@changed_cols))) . ")"; my @values = map { # when there is a column but no property, use NULL as the value defined($_) && $object_to_save->can($_) ? $object_to_save->$_ : undef } map { $class_object->property_for_column($_) || undef } (@changed_cols); if (scalar(@changed_cols) != scalar(@values)) { no warnings 'uninitialized'; my $mapping = join("\n", map { " $_ => ".$class_object->property_for_column($_) } @changed_cols); Carp::croak("Column count mismatch while inserting into table $table_name. " . "The table metadata expects to see ".scalar(@changed_cols) . " columns, but ".scalar(@values)." were retrieved from the object of type " . $object_to_save->class . ".\nCurrent column => property mapping:\n$mapping\n" . "There is probably a mismatch between the database column metadata and the column_name " . "property metadata"); } #grab fk_constraints so we can undef non primary-key nullable fks before delete my %non_pk_nullable_fk_columns = map { $_ => 1 } $self->get_non_primary_key_nullable_foreign_key_columns_for_table($table); if (%non_pk_nullable_fk_columns){ my @insert_values; my %update_values; for (my $i = 0; $i < @changed_cols; $i++){ my $col = $changed_cols[$i]; if ($non_pk_nullable_fk_columns{$col}) { push @insert_values, undef; $update_values{$col} = $values[$i]; }else{ push @insert_values, $values[$i]; } } push @commands, { type => 'insert', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@insert_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; ##$DB::single = 1; # %update_values can be empty if the Metadb is out of date, and has a fk constraint column # that no longer exists in the class metadata if (%update_values) { my @pk_values = $self->_id_values_for_primary_key($table, $object_to_save); my $where = $self->_matching_where_clause($table, \@pk_values); my @update_cols = keys %update_values; my @update_values = ((map {$update_values{$_}} @update_cols), @pk_values); my $update_sql = " UPDATE "; $update_sql .= "$table_name SET ". join(",", map { "$_ = ?" } @update_cols) . " WHERE $where"; push @commands, { type => 'update', table_name => $table_name, column_names => \@update_cols, sql => $update_sql, params => \@update_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } else { push @commands, { type => 'insert', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } else { die "Unknown action $table_action for $object_to_save" . Dumper($object_to_save) . "\n"; } } # next table return @commands; } sub _do_on_default_dbh { my $self = shift; my $method = shift; return 1 unless $self->has_default_handle(); my $dbh = $self->get_default_handle; unless ($dbh->$method(@_)) { $self->error_message("DataSource ".$self->get_name." failed to $method: ".$dbh->errstr); return undef; } return 1; } sub commit { my $self = shift; if ($self->has_default_handle) { if (my $dbh = $self->get_default_handle) { if ($dbh->{AutoCommit} ) { $self->debug_message('Ignoring ineffective commit because AutoCommit is on'); return 1; } } } $self->_do_on_default_dbh('commit', @_); } sub rollback { my $self = shift; if ($self->has_default_handle) { if (my $dbh = $self->get_default_handle) { if ($dbh->{AutoCommit} ) { $self->debug_message('Ignoring ineffective rollback because AutoCommit is on'); return 1; } } } $self->_do_on_default_dbh('rollback', @_); } sub disconnect { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } my $rv = $self->_do_on_default_dbh('disconnect', @_); $self->__invalidate_get_default_handle__; $self->is_connected(0); return $rv; } sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $parent_class_data = $self->SUPER::_generate_class_data_for_loading($class_meta); my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my $order_by_columns; do { my @id_column_names; for my $inheritance_class_name (@class_hierarchy) { my $inheritance_class_object = UR::Object::Type->get($inheritance_class_name); unless ($inheritance_class_object->table_name) { next; } @id_column_names = map { my $t = $inheritance_class_object->table_name; ($t) = ($t =~ /(\S+)\s*$/); $t . '.' . $_ } grep { defined } map { my $p = $inheritance_class_object->property_meta_for_name($_); Carp::croak("No property $_ found for " . $inheritance_class_object->class_name) unless $p; $p->column_name; } map { $_->property_name } grep { $_->column_name } $inheritance_class_object->direct_id_property_metas; last if (@id_column_names); } $order_by_columns = \@id_column_names; }; my @all_table_properties; my @direct_table_properties; my $first_table_name = $class_meta->first_table_name; my $sub_classification_method_name; my ($sub_classification_meta_class_name, $subclassify_by); my @base_joins; my $prev_table_name; my $prev_id_column_name; my %seen; for my $co ( $class_meta, @{ $parent_class_data->{parent_class_objects} } ) { next if $seen{ $co->class_name }++; my $table_name = $co->first_table_name; next unless $table_name; #$first_table_name ||= $co->table_name; $sub_classification_method_name ||= $co->sub_classification_method_name; $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_table_properties, map { [$co, $_, $table_name, 0 ] } sort $sort_sub grep { (defined $_->column_name && $_->column_name ne '') or (defined $_->calculate_sql && $_->calculate_sql ne '') } UR::Object::Property->get( class_name => $co->class_name ); @direct_table_properties = @all_table_properties if $class_meta eq $co; } my @lob_column_names; my @lob_column_positions; my $pos = 0; for my $class_property (@all_table_properties) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; my $data_type = $sql_property->data_type || ''; if ($data_type =~ /LOB$/i) { push @lob_column_names, $sql_property->column_name; push @lob_column_positions, $pos; } $pos++; } my $query_config; my $post_process_results_callback; if (@lob_column_names) { $query_config = $self->_prepare_for_lob; if ($query_config) { my $results_row_arrayref; my @lob_ids; my @lob_values; $post_process_results_callback = sub { $results_row_arrayref = shift; my $dbh = $self->get_default_handle; @lob_ids = @$results_row_arrayref[@lob_column_positions]; @lob_values = $self->_post_process_lob_values($dbh,\@lob_ids); @$results_row_arrayref[@lob_column_positions] = @lob_values; $results_row_arrayref; }; } } my $class_data = { %$parent_class_data, all_table_properties => \@all_table_properties, direct_table_properties => \@direct_table_properties, first_table_name => $first_table_name, sub_classification_method_name => $sub_classification_method_name, sub_classification_meta_class_name => $sub_classification_meta_class_name, subclassify_by => $subclassify_by, base_joins => \@base_joins, order_by_columns => $order_by_columns, lob_column_names => \@lob_column_names, lob_column_positions => \@lob_column_positions, query_config => $query_config, post_process_results_callback => $post_process_results_callback, }; return $class_data; } sub _select_clause_for_table_property_data { my $self = shift; my $column_data = $self->_select_clause_columns_for_table_property_data(@_); my $select_clause = join(', ',@$column_data); return $select_clause; } sub _select_clause_columns_for_table_property_data { my $self = shift; my @column_data; for my $class_property (@_) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; $sql_table_name ||= $sql_class->table_name; my ($select_table_name) = ($sql_table_name =~ /(\S+)\s*$/s); # FIXME - maybe a better way would be for these sql-calculated properties, the column_name() # or maybe some other related property name) is actually calculated, so this logic # gets encapsulated in there? if (my $sql_function = $sql_property->calculate_sql) { my @calculate_from = ref($sql_property->calculate_from) eq 'ARRAY' ? @{$sql_property->calculate_from} : ( $sql_property->calculate_from ); foreach my $sql_column_name ( @calculate_from ) { $sql_function =~ s/($sql_column_name)/$sql_table_name\.$1/g; } push(@column_data, $sql_function); } else { push(@column_data, $select_table_name . "." . $sql_property->column_name); } } return \@column_data; } # These seem to be standard for most RDBMSs my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'VARCHAR' => ['Text', undef], 'CHAR' => ['Text', 1], 'CHARACTER' => ['Text', 1], 'XML' => ['Text', undef], 'INTEGER' => ['Integer', undef], 'UNSIGNED INTEGER' => ['Integer', undef], 'SIGNED INTEGER' => ['Integer', undef], 'INT' => ['Integer', undef], 'LONG' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'SMALLINT' => ['Integer', undef], 'FLOAT' => ['Number', undef], 'NUMBER' => ['Number', undef], 'DOUBLE' => ['Number', undef], 'DECIMAL' => ['Number', undef], 'REAL' => ['Number', undef], 'BOOL' => ['Boolean', undef], 'BOOLEAN' => ['Boolean', undef], 'BIT' => ['Boolean', undef], 'DATE' => ['DateTime', undef], 'DATETIME' => ['DateTime', undef], 'TIMESTAMP' => ['DateTime', undef], 'TIME' => ['DateTime', undef], ); sub normalize_vendor_type { my ($class, $type) = @_; $type = uc($type); $type =~ s/\(\d+\)$//; return $type; } sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; $type = $class->normalize_vendor_type($type); my $urtype = $ur_data_type_for_vendor_data_type{$type}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } sub _vendor_data_type_for_ur_data_type { return ( TEXT => 'VARCHAR', STRING => 'VARCHAR', INTEGER => 'INTEGER', DECIMAL => 'INTEGER', NUMBER => 'FLOAT', BOOLEAN => 'INTEGER', DATETIME => 'DATETIME', TIMESTAMP => 'TIMESTAMP', __default__ => 'VARCHAR', ); } sub data_source_type_for_ur_data_type { my($class, $type) = @_; if ($type and $type->isa('UR::Value')) { ($type) =~ m/UR::Value::(\w+)/; } my %types = $class->_vendor_data_type_for_ur_data_type(); return $type && $types{uc($type)} ? $types{uc($type)} : $types{__default__}; } # Given two properties with different 'is', return a 2-element list of # SQL functions to apply to perform a comparison in the DB. 0th element # gets applied to the left side, 1st element to the right. This implementation # uses printf formats where the %s gets fed an SQL expression like # "table.column" # # SQLite basically treats everything as strings, so needs no conversion. # other DBs will have their own conversions # # $sql_clause will be one of "join", "where" sub cast_for_data_conversion { my($class, $left_type, $right_type, $operator, $sql_clause) = @_; return ('%s', '%s'); } sub do_after_fork_in_child { my $self = shift->_singleton_object; my $dbhs = $self->_all_dbh_hashref; for my $k (keys %$dbhs) { if ($dbhs->{$k}) { $dbhs->{$k}->{InactiveDestroy} = 1; delete $dbhs->{$k}; } } # reset our state back to being "disconnected" $self->__invalidate_get_default_handle__; $self->_all_dbh_hashref({}); $self->is_connected(0); # now force a reconnect $self->get_default_handle(); return 1; } sub parse_view_and_alias_from_inline_view { my($self, $sql) = @_; return ($sql and $sql =~ m/^(.*?)(?:\s+as)?\s+(\w+)\s*$/s) ? ($1, $2) : (); } 1; =pod =head1 NAME UR::DataSource::RDBMS - Abstract base class for RDBMS-type data sources =head1 DESCRIPTION This class implements the interface UR uses to query RDBMS databases with DBI. It encapsulates the system's knowledge of classes/properties relation to tables/columns, and how to generate SQL to create, retrieve, update and delete table rows that represent object instances. =head1 SEE ALSO UR::DataSource, UR::DataSource::Oracle, UR::DataSource::Pg, UR::DataSource::SQLite UR::DataSource::MySQL =cut BitmapIndex.pm100664023532023421 314112544604516 20373 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::BitmapIndex; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::BitmapIndex', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_bitmap_index', er_role => '', id_properties => [qw/data_source table_name bitmap_index_name/], properties => [ bitmap_index_name => { type => 'varchar', len => undef, sql => 'bitmap_index_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::BitmapIndex - metadata about a data source's bitmap indexes =head1 DESCRIPTION This class represents instances of bitmap indexes in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. The existence of bitmap indexes in a datasource affects SQL generation during a Context commit. Oracle's implementation requires a table covered by a bitmap index to be locked while it is being updated. =cut Entity.pm100664023532023421 112012544604516 17436 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::Entity; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::Entity', is => ['UR::Entity'], is_abstract => 1, data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::Meta::RDBMS::Entity - Parent class for all MetaDB-sourced classes =head1 DESCRIPTION This class exists as a means for flagging MetaDB objects and handling them specially by the infrastructure in certain circumstances, such as final data source determination. =cut FkConstraint.pm100664023532023421 1327512544604516 20625 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::FkConstraint; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::FkConstraint', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_fk_constraint', er_role => '', id_properties => [qw/data_source table_name r_table_name fk_constraint_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, fk_constraint_name => { type => 'varchar', len => undef, sql => 'fk_constraint_name' }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, r_owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'r_owner' }, r_table_name => { type => 'varchar', len => undef, sql => 'r_table_name' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, ], data_source => 'UR::DataSource::Meta', ); #UR::Object::Type->bootstrap_object(__PACKAGE__); sub _fk_constraint_column_class { if (shift->isa('UR::Object::Ghost')) { return 'UR::DataSource::RDBMS::FkConstraintColumn::Ghost'; } else { return 'UR::DataSource::RDBMS::FkConstraintColumn'; } } sub _table_classes { if (shift->isa('UR::Object::Ghost')) { return ('UR::DataSource::RDBMS::Table::Ghost', 'UR::DataSource::RDBMS::Table'); } else { return ('UR::DataSource::RDBMS::Table', 'UR::DataSource::RDBMS::Table::Ghost'); } } sub get_with_special_params { my($class,$rule,%args) = @_; #$DB::single = 1; my $column_name = delete $args{'column_name'}; my $r_column_name = delete $args{'r_column_name'}; my @fks = $class->get($rule); return $class->context_return(@fks) unless ($column_name || $r_column_name); my @objects; foreach my $fk ( @fks ) { my %fkc_args = ( data_source => $fk->data_source, table_name => $fk->table_name, r_table_name => $fk->r_table_name, ); $fkc_args{'column_name'} = $column_name if $column_name; $fkc_args{'r_column_name'} = $r_column_name if $r_column_name; my @fkc = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkc_args); push @objects,$fk if @fkc; } return $class->context_return(@objects); } sub create { my $class = shift; my $params = { $class->define_boolexpr(@_)->normalize->params_list }; my $column_name = delete $params->{'column_name'}; my $r_column_name = delete $params->{'r_column_name'}; if ($column_name || $r_column_name) { $column_name = [ $column_name ] unless (ref $column_name); $r_column_name = [ $r_column_name ] unless (ref $r_column_name); unless (scalar @$column_name == scalar @$r_column_name) { Carp::confess('column_name list and r_column_name list must be the same length'); return undef; } } my $self = $class->SUPER::create($params); while ($column_name && @$column_name) { my $col_name = shift @$column_name; my $r_col_name = shift @$r_column_name; my $col_class = $self->_fk_constraint_column_class; $col_class->create(data_source => $self->data_source, fk_constraint_name => $self->fk_constraint_name, table_name => $self->table_name, column_name => $col_name, r_table_name => $self->r_table_name, r_column_name => $r_col_name); } return $self; } sub get_related_column_objects { my($self,$prop_name) = @_; my @fkcs = UR::DataSource::RDBMS::FkConstraintColumn->get( data_source => $self->data_source, table_name => $self->table_name, r_table_name => $self->r_table_name, fk_constraint_name => $self->fk_constraint_name, ); return @fkcs unless $prop_name; return map { $_->$prop_name } @fkcs; } sub column_names { return shift->get_related_column_objects('column_name'); } sub r_column_names { return shift->get_related_column_objects('r_column_name'); } sub column_name_map { my $self = shift; my @fkcs = $self->get_related_column_objects(); return map { [ $_->column_name, $_->r_column_name ] } @fkcs; } sub _get_related_table { my($self,$table_name) = @_; foreach my $try_class ( $self->_table_classes ) { my $table = $try_class->get(data_source => $self->data_source, table_name => $table_name); return $table if $table; } return undef; } sub get_table { my $self = shift; return $self->_get_related_table($self->table_name); } sub get_r_table { my $self = shift; return $self->_get_related_table($self->r_table_name); } 1; =pod =head1 NAME UR::DataSource::RDBMS::FkConstraint - metadata about a data source's foreign keys =head1 DESCRIPTION This class represents instances of foreign keys in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. =cut FkConstraintColumn.pm100664023532023421 340312544604516 21753 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::FkConstraintColumn; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::FkConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_fk_constraint_column', er_role => 'bridge', id_properties => [qw/data_source table_name fk_constraint_name column_name/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, fk_constraint_name => { type => 'varchar', len => undef, sql => 'fk_constraint_name' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, r_column_name => { type => 'varchar', len => undef, sql => 'r_column_name' }, r_table_name => { type => 'varchar', len => undef, sql => 'r_table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::FkConstraintColumn - metadata about a data source's foreign keys =head1 DESCRIPTION This class represents the column linkages that make up a foreign key. Each instance has a column_name (the source, where the foreign key points from) and r_column_name (remote column name, where the fireign key points to), as well as the source and remote table names. =cut Between.pm100664023532023421 34012544604516 21331 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::Between; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql between ? and ?"; return ($sql, @$val); } 1; Equals.pm100664023532023421 61012544604516 21172 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::Equals; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql; my @sql_params; if (UR::DataSource::RDBMS->_value_is_null($val)) { $sql = "$expr_sql IS NULL"; } else { $sql = "$expr_sql = ?"; @sql_params = ($val); } return ($sql, @sql_params); } 1; False.pm100664023532023421 34712544604516 21001 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::False; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = qq(( $expr_sql IS NULL or $expr_sql = 0 )); return ($sql); } 1; GreaterOrEqual.pm100664023532023421 33312544604516 22624 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::GreaterOrEqual; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql >= ?"; return ($sql, $val); } 1; GreaterThan.pm100664023532023421 32712544604516 22151 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::GreaterThan; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql > ?"; return ($sql, $val); } 1; In.pm100664023532023421 316612544604516 20337 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::In; # This allows the size of an autogenerated IN-clause to be adjusted. # The limit for Oracle is 1000, and a bug requires that, in some cases # we drop to 250. use constant IN_CLAUSE_SIZE_LIMIT => 250; our @CARP_NOT = qw( UR::DataSource::RDBMS ); sub _negation_clause { '' } sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; unless (ref($val) eq 'ARRAY') { $val = [$val]; } unless (@$val) { # an empty list was passed-in. # since "in ()", like "where 1=0", is self-contradictory, # there is no data to return, and no SQL required Carp::carp("Null in-clause"); return; } my @list = do { no warnings 'uninitialized'; sort @$val; }; my $has_null = _list_contains_null(\@list); my $wrap = ($has_null or @$val > IN_CLAUSE_SIZE_LIMIT ? 1 : 0); my $cnt = 0; my $sql = ''; $sql .= "\n(\n " if $wrap; while (my @set = splice(@list,0,IN_CLAUSE_SIZE_LIMIT)) { $sql .= "\n or " if $cnt++; $sql .= $expr_sql . $class->_negation_clause . " in (" . join(",",map { UR::Util::sql_quote($_) } @set) . ")"; } if ($has_null) { $sql .= "\n or $expr_sql is " . $class->_negation_clause . ' null'; } $sql .= "\n)\n" if $wrap; return ($sql); } sub _list_contains_null { my $list = shift; foreach my $elt ( @$list ) { if (! defined($elt) or length($elt) == 0 ) { return 1; } } return ''; } 1; LessOrEqual.pm100664023532023421 33012544604516 22136 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::LessOrEqual; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql <= ?"; return ($sql, $val); } 1; LessThan.pm100664023532023421 32412544604516 21463 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::LessThan; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql < ?"; return ($sql, $val); } 1; Like.pm100664023532023421 41712544604516 20631 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::Like; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql like ?"; if ($escape) { $sql .= " escape $escape"; } return ($sql, $val); } 1; NotBetween.pm100664023532023421 34712544604516 22021 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::NotBetween; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql not between ? and ?"; return ($sql, @$val); } 1; NotEquals.pm100664023532023421 67112544604516 21662 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::NotEquals; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql; my @sql_params; if (UR::DataSource::RDBMS->_value_is_null($val)) { $sql = "$expr_sql IS NOT NULL"; } else { $sql = sprintf("( %s != ? or %s is null)", $expr_sql, $expr_sql); @sql_params = ($val); } return ($sql, @sql_params); } 1; NotIn.pm100664023532023421 27412544604516 20775 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::NotIn; use base 'UR::DataSource::RDBMS::Operator::In'; sub _negation_clause { ' not' }; # note the leading space 1; NotLike.pm100664023532023421 42612544604516 21312 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::NotLike; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = "$expr_sql not like ?"; if ($escape) { $sql .= " escape $escape"; } return ($sql, $val); } 1; True.pm100664023532023421 35412544604516 20664 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Operatoruse strict; use warnings; package UR::DataSource::RDBMS::Operator::True; sub generate_sql_for { my($class, $expr_sql, $val, $escape) = @_; my $sql = qq(( $expr_sql IS NOT NULL and $expr_sql != 0 )); return ($sql); } 1; PkConstraintColumn.pm100664023532023421 277412544604516 21777 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::PkConstraintColumn; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::PkConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_pk_constraint_column', er_role => '', id_properties => [qw/data_source table_name column_name rank/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, rank => { type => 'integer', len => undef, sql => 'rank' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::PkConstraintColumn - metadata about a data source's primary keys =head1 DESCRIPTION This class represents columns that make up a primary key. Tables with multiple-column primary keys are ordered by their 'rank' property. =cut Table.pm100664023532023421 1525212544604516 17244 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::Table; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::Table', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_table', id_properties => [qw/data_source table_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, er_type => { type => 'varchar', len => undef, sql => 'er_type', is_optional => 1 }, last_ddl_time => { type => 'timestamp', len => undef, sql => 'last_ddl_time', is_optional => 1 }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, remarks => { type => 'varchar', len => undef, is_optional => 1, sql => 'remarks' }, table_type => { type => 'varchar', len => undef, sql => 'table_type' }, ], data_source => 'UR::DataSource::Meta', ); sub _related_class_name { my($self,$subject) = @_; my $class = ref($self); # FIXME This seems kinda braindead, but is probably faster than using s/// # Is it really the right thing? my $pos = index($class, '::Table'); substr($class, $pos + 2, 5, $subject); # +2 to keep the "::" return $class; } sub _fk_constraint_class { return shift->_related_class_name('FkConstraint'); } sub _pk_constraint_class { return shift->_related_class_name('PkConstraintColumn'); } sub _unique_constraint_class { return shift->_related_class_name('UniqueConstraintColumn'); } sub _table_column_class { return shift->_related_class_name('TableColumn'); } sub _bitmap_index_class { return shift->_related_class_name('BitmapIndex'); } sub columns { my $self = shift; my $col_class = $self->_table_column_class; return $col_class->get(data_source => $self->data_source, table_name => $self->table_name); } sub column_names { return map { $_->column_name } shift->columns; } sub primary_key_constraint_columns { my $self = shift; my $pk_class = $self->_pk_constraint_class; my @pks = $pk_class->get(data_source => $self->data_source, table_name => $self->table_name); my @pks_with_rank = map { [ $_->rank, $_ ] } @pks; return map { $_->[1] } sort { $a->[0] <=> $b->[0] } @pks_with_rank; } sub primary_key_constraint_column_names { return map { $_->column_name } shift->primary_key_constraint_columns; } sub fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class; my @fks = $fk_class->get(data_source => $self->data_source, table_name => $self->table_name); return @fks; } sub fk_constraint_names { return map { $_->fk_constraint_name } shift->fk_constraints; } sub ref_fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class; my @fks = $fk_class->get(data_source => $self->data_source, r_table_name => $self->table_name); return @fks; } sub ref_fk_constraint_names { return map { $_->fk_constraint_name } shift->ref_fk_constraints; } sub unique_constraint_column_names { my($self,$constraint) = @_; my @c; if ($constraint) { @c = $self->unique_constraints(constraint_name => $constraint); } else { @c = $self->unique_constraints(); } my %names = map {$_->column_name => 1 } @c; return keys %names; } sub unique_constraint_names { my $self = shift; my %names = map { $_->constraint_name => 1 } $self->unique_constraints; return keys %names; } sub unique_constraints { my $self = shift; my $uc_class = $self->_unique_constraint_class; my @c = $uc_class->get( data_source => $self->data_source, table_name => $self->table_name, @_); return @c; } sub bitmap_indexes { my $self = shift; my $bi_class = $self->_bitmap_index_class; my @bi = $bi_class->get(data_source => $self->data_source, table_name => $self->table_name); return @bi; } sub bitmap_index_names { return map { $_->bitmap_index_name } shift->bitmap_indexes; } # FIXME Due to a "bug" in getting class objects, you need to pass in namespace => 'name' as # arguments to get this to work. sub handler_class { my $self = shift; return UR::Object::Type->get(table_name => $self->table_name, @_); } sub handler_class_name { my $self = shift; return $self->handler_class(@_)->class_name; } sub delete { my $self = shift; my @deleteme = ( $self->fk_constraints, $self->bitmap_indexes, $self->primary_key_constraint_columns, $self->columns, ); for my $obj ( @deleteme ) { $obj->delete; unless ($obj->isa('UR::DeletedRef')) { Carp::confess("Failed to delete $obj ".$obj->{'id'}); } } $self->SUPER::delete(); return $self; } 1; =pod =head1 NAME UR::DataSource::Meta::RDBMS::Table - Object-oriented class for RDBMS table objects. =head1 SYNOPSIS $t = UR::DataSource::Meta::RDBMS::Table->get( data_source => 'Namespace::DataSource::Name', table_name => 'MY_TABLE_NAME'); @c = $t->column_names; @f = $t->fk_constraint_names; =head1 DESCRIPTION Objects of this class represent a table in a database. They are primarily used by the class updating logic in the command line tool C, but can be retrieved and used in any application. Their instances come from from the MetaDB (L) which is partitioned and has one physical database per Namespace. =head2 Related Metadata Methods =over 4 =item @col_objs = $t->columns(); =item @col_names = $t->column_names(); =item @fk_objs = $t->fk_constraints(); =item @fk_names = $t->fk_constraint_names(); =item @ref_fk_objs = $t->ref_fk_constraints(); =item @ref_fk_names = $t->ref_fk_constraint_names(); =item @pk_objs = $t->primary_key_constraint_columns(); =item @pk_col_names = $t->primary_key_constraint_column_names(); =item @bi_objs = $t->bitmap_indexes(); =item @bi_names = $t->bitmap_index_names(); Return related metadata objects (or names) for the given table object. =back =cut Text.pm100664023532023421 116112544604516 22440 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/Table/View/Defaultpackage UR::DataSource::RDBMS::Table::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has_many => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['table_name', 'data_source', 'column_names'] }, ], ); 1; =pod =head1 NAME UR::DataSource::RDBMS::Table::View::Default::Text - View class for RDBMS table objects =head1 DESCRIPTION This class defines a text-mode view for RDBMS table objects, and is used by the 'ur info' command. =cut TableColumn.pm100664023532023421 725412544604516 20405 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::TableColumn; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::TableColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_table_column', er_role => '', id_properties => [qw/data_source table_name column_name/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, data_length => { type => 'varchar', len => undef, is_optional => 1, sql => 'data_length' }, data_type => { type => 'varchar', len => undef, sql => 'data_type' }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, nullable => { type => 'varchar', len => undef, sql => 'nullable' }, remarks => { type => 'varchar', len => undef, is_optional => 1, sql => 'remarks' }, ], data_source => 'UR::DataSource::Meta', ); # Methods moved over from the old App::DB::TableColumn sub _fk_constraint_class { my $self = shift; if (ref($self) =~ /::Ghost$/) { return "UR::DataSource::RDBMS::FkConstraint::Ghost" } else { return "UR::DataSource::RDBMS::FkConstraint" } } sub get_table { my $self = shift; my $table_name = $self->table_name; my $data_source = $self->data_source; $data_source or Carp::confess("Can't determine data_source for table $table_name column ".$self->column_name ); my $table = UR::DataSource::RDBMS::Table->get(table_name => $table_name, data_source => $data_source) || UR::DataSource::RDBMS::Table::Ghost->get(table_name => $table_name, data_source => $data_source); return $table; } sub fk_constraint_names { my @fks = shift->fk_constraints; return map { $_->fk_constraint_name } @fks; } sub fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class(); my @fks = $fk_class->get(table_name => $self->table_name, data_source => $self->data_source); return @fks; } sub bitmap_index_names { Carp::confess("not implemented yet?!"); } # the update classes code uses this. If the data type of a column is a time-ish format, then # the data_length reported by the schema is the number of bytes used internally by the DB. # Since the UR-object will store the time in text format, it will always be longer than # that. To keep $obj->__errors__ from complaining, don't even bother storing the length of # time-ish data sub is_time_data { my $self = shift; my $type = $self->data_type; if ($type =~ m/TIMESTAMP|DATE|INTERVAL/i) { return 1; } else { return; } } 1; =pod =head1 NAME UR::DataSource::RDBMS::TableColumn - metadata about a data source's table's columns =head1 DESCRIPTION This class represents instances of columns in a data source's tables. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. =cut Text.pm100664023532023421 121612544604516 23617 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMS/TableColumn/View/Defaultpackage UR::DataSource::RDBMS::TableColumn::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['column_name', 'table_name', 'data_type', 'length', 'nullable'] }, ], ); 1; =pod =head1 NAME UR::DataSource::RDBMS::TableColumn::View::Default::Text - View class for RDBMS column objects =head1 DESCRIPTION This class defines a text-mode view for RDBMS column objects, and is used by the 'ur info' command. =cut UniqueConstraintColumn.pm100664023532023421 326112544604516 22663 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::UniqueConstraintColumn; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::UniqueConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_unique_constraint_column', id_properties => [qw/data_source table_name constraint_name column_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, sql => 'owner', is_optional => 1 }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, constraint_name => { type => 'varchar', len => undef, sql => 'constraint_name' }, column_name => { type => 'varchar', len => undef, sql => 'column_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::UniqueConstraintColumn - metadata about a data source's unique constraints =head1 DESCRIPTION This class represents instances of unique constraints in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. Multi-column unique constraints are represented by instances having the same table_name and constraint_name, but different column_names. =cut RDBMSRetriableOperations.pm100664023532023421 1137412544604516 22114 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::RDBMSRetriableOperations; use strict; use warnings; use Time::HiRes; # A mixin class that provides methods to retry queries and syncs # # Consumers should provide should_retry_operation_after_error(). # It's passed the SQL that generated the error and the DBI error string. # It should return true if the operation generating that error should be # retried. class UR::DataSource::RDBMSRetriableOperations { has_optional => [ retry_sleep_start_sec => { is => 'Integer', value => 1, doc => 'Initial inter-error sleep time' }, retry_sleep_max_sec => { is => 'Integer', value => 3600, doc => 'Maximum inter-error sleep time' }, ], valid_signals => ['retry'] }; # The guts of the thing. Consumers that want a base-datasource method to # be retriable should override the method to call this instead, and pass # a code ref to perform the retriable action sub _retriable_operation { my $self = UR::Util::object(shift); my $code = shift; $self->_make_retriable_operation_observer(); RETRY_LOOP: for( my $db_retry_sec = $self->retry_sleep_start_sec; $db_retry_sec < $self->retry_sleep_max_sec; $db_retry_sec *= 2 ) { my @rv = eval { $code->(); }; if ($@) { if ($@ =~ m/DB_RETRY/) { $self->error_message("DB_RETRY"); $self->debug_message("Disconnecting and sleeping for $db_retry_sec seconds...\n"); $self->disconnect_default_handle; Time::HiRes::sleep($db_retry_sec); $self->__signal_observers__('retry', $db_retry_sec); next RETRY_LOOP; } Carp::croak($@); # re-throw other exceptions } return $self->context_return(@rv); } die "Maximum database retries reached"; } { my %retry_observers; sub _make_retriable_operation_observer { my $self = shift; unless ($retry_observers{$self->class}++) { for (qw(query_failed commit_failed do_failed connect_failed sequence_nextval_failed)) { $self->add_observer( aspect => $_, priority => 99999, # Super low priority to fire last callback => \&_db_retry_observer, ); } } } } # Default is to not retry sub should_retry_operation_after_error { my($self, $sql, $dbi_errstr) = @_; return 0; } # The callback for the retry observer sub _db_retry_observer { my($self, $aspect, $db_operation, $sql, $dbi_errstr) = @_; unless (defined $sql) { $sql = '(no sql)'; } $self->error_message("SQL failed during $db_operation\nerror: $dbi_errstr\nsql: $sql"); die "DB_RETRY" if $self->should_retry_operation_after_error($sql, $dbi_errstr); # just fall off the end here... # Code triggering the observer will throw an exception } # Searches the parentage of $self for a RDBMS datasource class # and returns a ref to the named sub in that package # This is necessary because we're using a mixin class and not # a real role my %cached_rdbms_datasource_method_for; sub rdbms_datasource_method_for { my $self = shift; my $method = shift; my $target_class_name = shift; $target_class_name ||= $self->class; if ($cached_rdbms_datasource_method_for{$target_class_name}) { return $cached_rdbms_datasource_method_for{$target_class_name}->can($method); } foreach my $parent_class_name ( $target_class_name->__meta__->parent_class_names ) { if ( $parent_class_name->isa('UR::DataSource::RDBMS') ) { if ($parent_class_name->isa(__PACKAGE__) ) { if (my $sub = $self->rdbms_datasource_method_for($method, $parent_class_name)) { return $sub; } } else { $cached_rdbms_datasource_method_for{$target_class_name} = $parent_class_name; return $parent_class_name->can($method); } } } return; } # The retriable methods we want to wrap foreach my $parent_method (qw( create_iterator_closure_for_rule create_default_handle _sync_database do_sql autogenerate_new_object_id_for_class_name_and_rule )) { my $override = sub { my $self = shift; my @params = @_; # Installing this as the $parent_method leads to infinte recursion if # the parent does not directly inherit this class. use warnings FATAL => qw( recursion ); my $parent_sub ||= $self->rdbms_datasource_method_for($parent_method); $self->_retriable_operation(sub { $self->$parent_sub(@params); }); }; Sub::Install::install_sub({ into => __PACKAGE__, as => $parent_method, code => $override, }); } 1; SQLite.pm100664023532023421 10712312544604516 16526 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::SQLite; use strict; use warnings; use IO::Dir; use File::Spec; use File::Basename; use version; =pod =head1 NAME UR::DataSource::SQLite - base class for datasources using the SQLite3 RDBMS =head1 SYNOPSIS In the shell: ur define datasource sqlite Or write the singleton to represent the source directly: class Acme::DataSource::MyDB1 { is => 'UR::DataSource::SQLite', has_constant => [ server => '/var/lib/acme-app/mydb1.sqlitedb' ] }; You may also use a directory containing *.sqlite3 files. The primary database must be named main.sqlite3. All the other *.sqlite3 files are attached when the database is opened. class Acme::DataSource::MyDB2 { is => 'UR::DataSource::SQLite', has_constant => [ server => '/path/to/directory/' ] }; =cut require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::SQLite', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "SQLite" } sub default_owner { return 'main'; } sub owner { default_owner() } sub login { undef } sub auth { undef } sub create_default_handle { my $self = shift->_singleton_object(); $self->_init_database; if ($self->_db_path_specifies_a_directory($self->server)) { return $self->_create_default_handle_from_directory(); } else { return $self->SUPER::create_default_handle(@_); } } sub _create_default_handle_from_directory { my $self = shift; my $server_directory = $self->server; my $ext = $self->_extension_for_db; my $main_schema_file = File::Spec->catfile($server_directory, "main${ext}"); -f $main_schema_file || UR::Util::touch_file($main_schema_file) || die "Could not create main schema file $main_schema_file: $!"; my $server_sub_name = join('::', ref($self), 'server'); my $dbh = do { no strict 'refs'; no warnings 'redefine'; local *$server_sub_name = sub { $main_schema_file }; $self->SUPER::create_default_handle(); }; $self->_attach_all_schema_files_in_directory($dbh, $server_directory); return $dbh; } sub _attach_all_schema_files_in_directory { my($self, $dbh, $server_directory) = @_; my @schema_files = $self->_schema_files_in_directory($server_directory); local $dbh->{AutoCommit} = 1; my $main_db_file = join('', 'main', $self->_extension_for_db); foreach my $file ( @schema_files ) { next if $file eq $main_db_file; my $schema = $self->_schema_from_schema_filename($file); my $pathname = File::Spec->catfile($server_directory, $file); $dbh->do("ATTACH DATABASE '$pathname' as $schema") || Carp::croak("Could not attach schema file $file: ".$dbh->errstr); } } sub _schema_files_in_directory { my($self, $dir) = @_; my $dh = IO::Dir->new($dir); my @files; while (my $name = $dh->read) { my $pathname = File::Spec->catfile($dir, $name); next unless -f $pathname; push(@files, $name) if $self->_schema_from_schema_filename($name); } return @files; } sub _schema_from_schema_filename { my($self, $pathname) = @_; my($schema, $dir, $ext) = File::Basename::fileparse($pathname, $self->_extension_for_db); return $ext ? $schema : undef; } sub database_exists { my $self = shift; return 1 if -e $self->server; return 1 if -e $self->_data_dump_path; # exists virtually, and will dynamicaly instantiate return; } sub create_database { my $self = shift; die "Database exists!" if $self->database_exists; my $path = $self->server; return 1 if IO::File->new(">$path"); } sub can_savepoint { 0;} # Dosen't support savepoints # SQLite API sub _schema_path { return shift->_database_file_path() . '-schema'; } sub _data_dump_path { return shift->_database_file_path() . '-dump'; } # FIXME is there a way to make this an object parameter instead of a method sub server { my $self = shift->_singleton_object(); my $path = $self->__meta__->module_path; my $ext = $self->_extension_for_db; $path =~ s/\.pm$/$ext/ or Carp::croak("Odd module path $path. Expected something endining in '.pm'"); my $dir = File::Basename::dirname($path); return $path; } *_database_file_path = \&server; sub _extension_for_db { '.sqlite3'; } sub _journal_file_path { my $self = shift->_singleton_object(); return $self->server . "-journal"; } sub _init_database { my $self = shift->_singleton_object(); my $db_file = $self->server; my $dump_file = $self->_data_dump_path; my $schema_file = $self->_schema_path; my $db_time = (stat($db_file))[9]; my $dump_time = (stat($dump_file))[9]; my $schema_time = (stat($schema_file))[9]; if ($schema_time && ((-e $db_file and $schema_time > $db_time) or (-e $dump_file and $schema_time > $dump_time))) { $self->warning_message("Schema file is newer than the db file or the dump file. Replacing db_file $db_file."); my $dbbak_file = $db_file . '-bak'; my $dumpbak_file = $dump_file . '-bak'; unlink $dbbak_file if -e $dbbak_file; unlink $dumpbak_file if -e $dumpbak_file; rename $db_file, $dbbak_file if -e $db_file; rename $dump_file, $dumpbak_file if -e $dump_file; if (-e $db_file) { Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!"; } if (-e $dump_file) { Carp::croak "Failed to move out-of-date file $dump_file out of the way for reconstruction! $!"; } } if (-e $db_file) { if ($dump_time && ($db_time < $dump_time)) { my $bak_file = $db_file . '-bak'; $self->warning_message("Dump file is newer than the db file. Replacing db_file $db_file."); unlink $bak_file if -e $bak_file; rename $db_file, $bak_file; if (-e $db_file) { Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!"; } } } # NOTE: don't make this an "else", since we might go into both branches because we delete the file above. unless (-e $db_file) { # initialize a new database from the one in the base class # should this be moved to connect time? # TODO: auto re-create things as needed based on timestamp if (-e $dump_file) { # create from dump $self->warning_message("Re-creating $db_file from $dump_file."); $self->_load_db_from_dump_internal($dump_file); unless (-e $db_file) { Carp::croak("Failed to import $dump_file into $db_file!"); } } elsif ( (not -e $db_file) and (-e $schema_file) ) { # create from schema $self->warning_message("Re-creating $db_file from $schema_file."); $self->_load_db_from_dump_internal($schema_file); unless (-e $db_file) { Carp::croak("Failed to import $dump_file into $db_file!"); } } elsif ($self->class ne __PACKAGE__) { # copy from the parent class (disabled) Carp::croak("No schema or dump file found for $db_file.\n Tried schema path $schema_file\n and dump path $dump_file\nIf you still have *sqlite3n* SQLite database files please rename them to *sqlite3*, without the 'n'"); my $template_database_file = $self->SUPER::server(); unless (-e $template_database_file) { Carp::croak("Missing template database file: $db_file! Cannot initialize database for " . $self->class); } unless(File::Copy::copy($template_database_file,$db_file)) { Carp::croak("Error copying $db_file to $template_database_file to initialize database!"); } unless(-e $db_file) { Carp::croak("File $db_file not found after copy from $template_database_file. Cannot initialize database!"); } } else { Carp::croak("No db file found, and no dump or schema file found from which to re-construct a db file!"); } } return 1; } *_init_created_dbh = \&init_created_handle; sub init_created_handle { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; # wait one minute busy timeout $dbh->func(1800000,'busy_timeout'); return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(sqlite|\$|URMETA)/; } sub _get_sequence_name_for_table_and_column { my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_seq', $table_name, $column_name); unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTOINCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } # Overriding this so we can force the schema to 'main' for older versions of SQLite # # NOTE: table_info (called by SUPER::get_table_details_from_data_dictionary) in older # versions of DBD::SQLite does not return data for tables in other attached databases. # # This probably isn't an issue... Due to the limited number of people using older DBD::SQLite # (of particular note is that OSX 10.5 and earlier use such an old version), interseted with # the limited number of people using attached databases, it's probably not a problem. # The commit_between_schemas test does do this. If it turns out it is a problem, we could # appropriate the code from recent DBD::SQLite::table_info sub get_table_details_from_data_dictionary { my $self = shift; my $sth = $self->SUPER::get_table_details_from_data_dictionary(@_); my $sqlite_version = version->parse($DBD::SQLite::VERSION); my $needed_version = version->parse("1.26_04"); if ($sqlite_version >= $needed_version || !$sth) { return $sth; } my($catalog,$schema,$table_name) = @_; my @tables; my @returned_names; while (my $info = $sth->fetchrow_hashref()) { #@returned_names ||= (keys %$info); unless (@returned_names) { @returned_names = keys(%$info); } $info->{'TABLE_SCHEM'} ||= 'main'; push @tables, $info; } my $dbh = $self->get_default_handle(); my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); unless (@returned_names) { @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS ); } my $returned_sth = $sponge->prepare("table_info $table_name", { rows => [ map { [ @{$_}{@returned_names} ] } @tables ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # DBD::SQLite doesn't implement column_info. This is the UR::DataSource version of the same thing sub get_column_details_from_data_dictionary { my($self,$catalog,$schema,$table,$column) = @_; my $dbh = $self->get_default_handle(); # Convert the SQL wildcards to regex wildcards $column = '' unless defined $column; $column =~ s/%/.*/; $column =~ s/_/./; my $column_regex = qr(^$column$); my $sth_tables = $dbh->table_info($catalog, $schema, $table, 'TABLE'); my @table_names = map { $_->{'TABLE_NAME'} } @{ $sth_tables->fetchall_arrayref({}) }; my $override_owner; if ($DBD::SQLite::VERSION < 1.26_04) { $override_owner = 'main'; } my @columns; foreach my $table_name ( @table_names ) { my $sth = $dbh->prepare("PRAGMA table_info($table_name)") or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); $sth->execute() or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); while (my $info = $sth->fetchrow_hashref()) { next unless $info->{'name'} =~ m/$column_regex/; # SQLite doesn't parse our that type varchar(255) actually means type varchar size 255 my $data_type = $info->{'type'}; my $column_size; if ($data_type =~ m/(\S+)\s*\((\S+)\)/) { $data_type = $1; $column_size = $2; } my $node = {}; $node->{'TABLE_CAT'} = $catalog; $node->{'TABLE_SCHEM'} = $schema || $override_owner; $node->{'TABLE_NAME'} = $table_name; $node->{'COLUMN_NAME'} = $info->{'name'}; $node->{'DATA_TYPE'} = $data_type; $node->{'TYPE_NAME'} = $data_type; $node->{'COLUMN_SIZE'} = $column_size; $node->{'NULLABLE'} = ! $info->{'notnull'}; $node->{'IS_NULLABLE'} = ($node->{'NULLABLE'} ? 'YES' : 'NO'); $node->{'REMARKS'} = ""; $node->{'SQL_DATA_TYPE'} = ""; # FIXME shouldn't this be something related to DATA_TYPE $node->{'SQL_DATETIME_SUB'} = ""; $node->{'CHAR_OCTET_LENGTH'} = undef; # FIXME this should be the same as column_size, right? $node->{'ORDINAL_POSITION'} = $info->{'cid'}; $node->{'COLUMN_DEF'} = $info->{'dflt_value'}; # Remove starting and ending 's that appear erroneously with string default values $node->{'COLUMN_DEF'} =~ s/^'|'$//g if defined ( $node->{'COLUMN_DEF'}); push @columns, $node; } } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE ); my $returned_sth = $sponge->prepare("column_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @columns ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # SQLite doesn't store the name of a foreign key constraint in its metadata directly. # We can guess at it from the SQL used in the table creation. These regexes are probably # sloppy. We could replace them if there were a good SQL parser. sub _resolve_fk_name { my($self, $table_name, $column_list, $r_table_name, $r_column_list) = @_; if (@$column_list != @$r_column_list) { Carp::confess('There are '.scalar(@$column_list).' pk columns and '.scalar(@$r_column_list).' fk columns'); } my($table_info) = $self->_get_info_from_sqlite_master($table_name, 'table'); return unless $table_info; my $col_str = $table_info->{'sql'}; $col_str =~ s/^\s+|\s+$//g; # Remove leading and trailing whitespace $col_str =~ s/\s{2,}/ /g; # Remove multiple spaces if ($col_str =~ m/^CREATE TABLE (\w+)\s*?\((.*?)\)$/is) { unless ($1 eq $table_name) { Carp::croak("Table creation SQL for $table_name is inconsistent. Didn't find table name '$table_name' in string '$col_str'. Found $1 instead."); } $col_str = $2; } else { Carp::croak("Couldn't parse SQL for $table_name"); } my $fk_name; if (@$column_list > 1) { # Multiple column FKs must be specified as a table-wide constraint, and has a well-known format my $fk_list = '\s*' . join('\s*,\s*', @$column_list) . '\s*'; my $uk_list = '\s*' . join('\s*,\s*', @$r_column_list) . '\s*'; my $expected_to_find = sprintf('FOREIGN KEY\s*\(%s\) REFERENCES %s\s*\(%s\)', $fk_list, $r_table_name, $uk_list); my $regex = qr($expected_to_find)i; if ($col_str =~ m/$regex/) { ($fk_name) = ($col_str =~ m/CONSTRAINT (\w+) FOREIGN KEY\s*\($fk_list\)/i); } else { # Didn't find anything... return; } } else { # single-column FK constraints can be specified a couple of ways... # First, try as a table-wide constraint my $col = $column_list->[0]; my $r_col = $r_column_list->[0]; if ($col_str =~ m/FOREIGN KEY\s*\($col\)\s*REFERENCES $r_table_name\s*\($r_col\)/i) { ($fk_name) = ($col_str =~ m/CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\($col\)/i); } else { while ($col_str) { # Try parsing each of the column definitions # commas can't appear in here except to separate each column, right? my $this_col; if ($col_str =~ m/^(.*?)\s*,\s*(.*)/) { $this_col = $1; $col_str = $2; } else { $this_col = $col_str; $col_str = ''; } my($col_name, $col_type) = ($this_col =~ m/^(\w+) (\w+)/); next unless ($col_name and $col_name eq $col); if ($this_col =~ m/REFERENCES $r_table_name\s*\($r_col\)/i) { # It's the right column, and there's a FK constraint on it # Did the FK get a name? ($fk_name) = ($this_col =~ m/CONSTRAINT (\w+) REFERENCES/i); last; } else { # It's the right column, but there's no FK return; } } } } # The constraint didn't have a name. Make up something that'll likely be unique $fk_name ||= join('_', $table_name, @$column_list, $r_table_name, @$r_column_list, 'fk'); return $fk_name; } # We'll only support specifying $fk_table or $pk_table but not both # $fk_table refers to the table where the fk is attached # $pk_table refers to the table the pk points to - where the primary key exists sub get_foreign_key_details_from_data_dictionary { my($self, $pk_catalog, $pk_schema, $pk_table, $fk_catalog, $fk_schema, $fk_table) = @_; # first, build a data structure to collect columns of the same foreign key together my @returned_fk_info; if ($fk_table) { @returned_fk_info = $self->_get_foreign_key_details_for_fk_table_name($fk_schema, $fk_table); } elsif ($pk_table) { # We'll have to loop through each table in the DB and find FKs that reference # the named table my @tables = $self->_get_info_from_sqlite_master(undef,'table'); TABLE: foreach my $table_data ( @tables ) { my $from_table = $table_data->{'table_name'}; push @returned_fk_info, $self->_get_foreign_key_details_for_fk_table_name($fk_schema, $from_table, sub { $_[0]->{table} eq $pk_table }); } } else { Carp::croak("Can't get_foreign_key_details_from_data_dictionary(): either pk_table ($pk_table) or fk_table ($fk_table) are required"); } my $dbh = $self->get_default_handle; my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( UK_TABLE_CAT UK_TABLE_SCHEM UK_TABLE_NAME UK_COLUMN_NAME FK_TABLE_CAT FK_TABLE_SCHEM FK_TABLE_NAME FK_COLUMN_NAME ORDINAL_POSITION UPDATE_RULE DELETE_RULE FK_NAME UK_NAME DEFERABILITY ); my $table = $pk_table || $fk_table; my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @returned_fk_info ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # used by _get_foreign_key_details_for_fk_table_name to convert the on_delete or on_update # string into the number code commonly returnd by DBI my %update_delete_action_to_numeric_code = ( CASCADE => 0, RESTRICT => 1, 'SET NULL' => 2, 'NO ACTION' => 3, 'SET DEFAULT' => 4, ); sub _get_foreign_key_details_for_fk_table_name { my($self, $fk_schema_name, $fk_table_name, $accept_rows) = @_; $accept_rows ||= sub { 1 }; # default is accept all $fk_schema_name ||= 'main'; my $qualified_table_name = join('.', $fk_schema_name, $fk_table_name); my $dbh = $self->get_default_handle; my $fksth = $dbh->prepare("PRAGMA foreign_key_list($fk_table_name)") or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); unless ($fksth->execute()) { $self->error_message("foreign_key_list execute failed: $DBI::errstr"); return; } my @fk_rows_this_table; my(@column_list, @r_column_list); while (my $row = $fksth->fetchrow_hashref) { next unless ($accept_rows->($row)); my %fk_info_row = ( FK_TABLE_NAME => $fk_table_name, FK_TABLE_SCHEM => $fk_schema_name, UK_TABLE_SCHEM => $fk_schema_name, # SQLite doesn't tell us what attached DB it's from, so we'll guess UPDATE_RULE => $update_delete_action_to_numeric_code{$row->{on_update}}, DELETE_RULE => $update_delete_action_to_numeric_code{$row->{on_delete}}, ORDINAL_POSITION => $row->{seq} + 1, ); @fk_info_row{'FK_COLUMN_NAME','UK_TABLE_NAME','UK_COLUMN_NAME'} = @$row{'from','table','to'}; push @fk_rows_this_table, \%fk_info_row; push @column_list, $row->{from}; push @r_column_list, $row->{to} } if (@fk_rows_this_table) { my $fk_name = $self->_resolve_fk_name($fk_rows_this_table[0]->{FK_TABLE_NAME}, \@column_list, $fk_rows_this_table[0]->{UK_TABLE_NAME}, # They'll all have the same table, right? \@r_column_list); foreach my $fk_info_row ( @fk_rows_this_table ) { $fk_info_row->{FK_NAME} = $fk_name; } @fk_rows_this_table = sort { $a->{ORDINAL_POSITION} <=> $b->{ORDINAL_POSITION} } @fk_rows_this_table; } return @fk_rows_this_table; } sub get_bitmap_index_details_from_data_dictionary { # SQLite dosen't support bitmap indicies, so there aren't any return []; } sub get_unique_index_details_from_data_dictionary { my($self, $owner_name, $table_name) = @_; my $dbh = $self->get_default_handle(); return undef unless $dbh; my($index_list_fcn, $index_info_fcn) = ('index_list','index_info'); if ($owner_name) { $index_list_fcn = "${owner_name}.${index_list_fcn}"; $index_info_fcn = "${owner_name}.${index_info_fcn}"; } my $idx_sth = $dbh->prepare(qq(PRAGMA ${index_list_fcn}($table_name))); return undef unless $idx_sth; $idx_sth->execute(); my $ret = {}; while(my $data = $idx_sth->fetchrow_hashref()) { next unless ($data->{'unique'}); my $idx_name = $data->{'name'}; my $idx_item_sth = $dbh->prepare(qq(PRAGMA ${index_info_fcn}($idx_name))); $idx_item_sth->execute(); while(my $index_item = $idx_item_sth->fetchrow_hashref()) { $ret->{$idx_name} ||= []; push( @{$ret->{$idx_name}}, $index_item->{'name'}); } } return $ret; } # By default, make a text dump of the database at commit time. # This should really be a datasource property sub dump_on_commit { 0; } # We're overriding commit from UR::DS::commit() to add the behavior that after # the actual commit happens, we also make a dump of the database in text format # so that can be version controlled sub commit { my $self = shift; my $has_no_pending_trans = (!-f $self->_journal_file_path()); my $worked = $self->SUPER::commit(@_); return unless $worked; my $db_filename = $self->server(); my $dump_filename = $self->_data_dump_path(); return 1 if ($has_no_pending_trans); return 1 unless $self->dump_on_commit or -e $dump_filename; return $self->_dump_db_to_file_internal(); } # Get info out of the sqlite_master table. Returns a hashref keyed by 'name' # columns are: # type - 'table' or 'index' # name - Name of the object # table_name - name of the table this object references. For tables, it's the same as name, # for indexes, it's the name of the table it's indexing # rootpage - Used internally by sqlite # sql - The sql used to create the thing sub _get_info_from_sqlite_master { my($self, $name,$type) = @_; my($schema, @where, @exec_values); if ($name) { ($schema, $name) = $self->_resolve_owner_and_table_from_table_name($name); push @where, 'name = ?'; push @exec_values, $name; } if ($type) { push @where, 'type = ?'; push @exec_values, $type; } my $sqlite_master_table = $schema ? "${schema}.sqlite_master" : 'sqlite_master'; my $sql = "select * from $sqlite_master_table"; if (@where) { $sql .= ' where '.join(' and ', @where); } my $dbh = $self->get_default_handle(); my $sth = $dbh->prepare($sql); unless ($sth) { no warnings; $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr); return; } unless ($sth->execute(@exec_values)) { no warnings; $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr); return; } my @rows; while (my $row = $sth->fetchrow_arrayref()) { my $item; @$item{'type','name','table_name','rootpage','sql'} = @$row; # Force all names to lower case so we can find them later push @rows, $item; } return @rows; } # This is used if, for whatever reason, we can't sue the sqlite3 command-line # program to load up the database. We'll make a good-faith effort to parse # the SQL text, but it won't be fancy. This is intended to be used to initialize # meta DB dumps, so we should have to worry about escaping quotes, multi-line # statements, etc. # # The real DB file should be moved out of the way before this is called. The existing # DB file will be removed. sub _load_db_from_dump_internal { my $self = shift; my $file_name = shift; my $fh = IO::File->new($file_name); unless ($fh) { Carp::croak("Can't open DB dump file $file_name: $!"); } my $db_file = $self->server; if (-f $db_file) { unless(unlink($db_file)) { Carp::croak("Can't remove DB file $db_file: $!"); } } my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file",'','',{ AutoCommit => 0, RaiseError => 0 }); unless($dbh) { Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr"); } my $dump_file_contents = do { local( $/ ) ; <$fh> }; my @sql = split(';',$dump_file_contents); for (my $i = 0; $i < @sql; $i++) { my $sql = $sql[$i]; next unless ($sql =~ m/\S/); # Skip blank lines next if ($sql =~ m/BEGIN TRANSACTION|COMMIT/i); # We're probably already in a transaction # Is it restoring the foreign_keys setting? if ($sql =~ m/PRAGMA foreign_keys\s*=\s*(\w+)/) { my $value = $1; my $fk_setting = $self->_get_foreign_key_setting($dbh); if (! defined($fk_setting)) { # This version of SQLite cannot enforce foreign keys. # Print a warning message if they're trying to turn it on. # also, remember the setting so we can preserve its value # in _dump_db_to_file_internal() $self->_cache_foreign_key_setting_from_file($value); if ($value ne 'OFF') { $self->warning_message("Data source ".$self->id." does not support foreign key enforcement, but the dump file $db_file attempts to turn it on"); } next; } } unless ($dbh->do($sql)) { Carp::croak("Error processing SQL statement $i from DB dump file:\n$sql\nDBI error was: $DBI::errstr\n"); } } $dbh->commit(); $dbh->disconnect(); return 1; } sub _cache_foreign_key_setting_from_file { my $self = shift; our %foreign_key_setting_from_file; my $id = $self->id; if (@_) { $foreign_key_setting_from_file{$id} = shift; } return $foreign_key_setting_from_file{$id}; } # Is foreign key enforcement on or off? # returns undef if this version of SQLite cannot enforce foreign keys sub _get_foreign_key_setting { my $self = shift; my $dbh = shift; my $id = $self->id; our %foreign_key_setting; unless (exists $foreign_key_setting{$id}) { $dbh ||= $self->get_default_handle; my @row = $dbh->selectrow_array('PRAGMA foreign_keys'); $foreign_key_setting{$id} = $row[0]; } return $foreign_key_setting{$id}; } sub _resolve_order_by_clause_for_column { my($self, $column_name, $query_plan, $property_meta) = @_; my $is_optional = $property_meta->is_optional; my $column_clause = $column_name; # default, usual case if ($is_optional) { if ($query_plan->order_by_column_is_descending($column_name)) { $column_clause = "CASE WHEN $column_name ISNULL THEN 0 ELSE 1 END, $column_name DESC"; } else { $column_clause = "CASE WHEN $column_name ISNULL THEN 1 ELSE 0 END, $column_name"; } } elsif ($query_plan->order_by_column_is_descending($column_name)) { $column_clause = $column_name . ' DESC'; } return $column_clause; } sub _dump_db_to_file_internal { my $self = shift; my $fk_setting = $self->_get_foreign_key_setting(); my $file_name = $self->_data_dump_path(); unless (-w $file_name) { # dump file isn't writable... return 1; } my $fh = IO::File->new($file_name, '>'); unless ($fh) { Carp::croak("Can't open DB dump file $file_name for writing: $!"); } my $db_file = $self->server; my $dbh = $self->get_default_handle; unless ($dbh) { Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr"); } if (defined $fk_setting) { # Save the value of the foreign_keys setting, if it's supported $fh->print('PRAGMA foreign_keys = ' . ( $fk_setting ? 'ON' : 'OFF' ) .";\n"); } else { # If not supported, but if _load_db_from_dump_internal came across the value, preserve it $fk_setting = $self->_cache_foreign_key_setting_from_file; if (defined $fk_setting) { $fh->print("PRAGMA foreign_keys = $fk_setting;\n"); } } $fh->print("BEGIN TRANSACTION;\n"); my @tables = $self->_get_table_names_from_data_dictionary(); foreach my $qualified_table ( @tables ) { my(undef, $table) = $self->_resolve_owner_and_table_from_table_name($qualified_table); my($item_info) = $self->_get_info_from_sqlite_master($table); my $creation_sql = $item_info->{'sql'}; $creation_sql .= ";" unless(substr($creation_sql, -1, 1) eq ";"); $creation_sql .= "\n" unless(substr($creation_sql, -1, 1) eq "\n"); $fh->print($creation_sql); if ($item_info->{'type'} eq 'table') { my $sth = $dbh->prepare("select * from $table"); unless ($sth) { Carp::croak("Can't retrieve data from table $table: $DBI::errstr"); } unless($sth->execute()) { Carp::croak("execute() failed while retrieving data for table $table: $DBI::errstr"); } while(my @row = $sth->fetchrow_array) { foreach my $col ( @row ) { if (! defined $col) { $col = 'null'; } elsif ($col =~ m/\D/ or length($col) == 0) { $col = "'" . $col . "'"; # Put quotes around non-numeric stuff } } $fh->printf("INSERT INTO %s VALUES(%s);\n", $table, join(',', @row)); } } } $fh->print("COMMIT;\n"); $fh->close(); $dbh->disconnect(); return 1; } sub _create_dbh_for_alternate_db { my($self, $connect_string) = @_; my $match_dbname = qr{dbname=([^;]+)}i; my($db_file) = $connect_string =~ m/$match_dbname/; $db_file || Carp::croak("Cannot determine dbname for alternate DB from dbi connect string $connect_string"); if ($self->_db_path_specifies_a_directory($db_file)) { mkdir $db_file; my $main_schema_file = join('', 'main', $self->_extension_for_db); $db_file = File::Spec->catfile($db_file, $main_schema_file); $connect_string =~ s/$match_dbname/dbname=$db_file/; } my $dbh = $self->SUPER::_create_dbh_for_alternate_db($connect_string); return $dbh; } sub _db_path_specifies_a_directory { my($self, $pathname) = @_; return (-d $pathname) || ($pathname =~ m{/$}); } sub _assure_schema_exists_for_table { my($self, $table_name, $dbh) = @_; $dbh ||= $self->get_default_handle; my($schema_name, undef) = $self->_extract_schema_and_table_name($table_name); if ($schema_name and ! $self->is_schema_attached($schema_name, $dbh) ) { # pretend we have schemas my($main_filename) = $dbh->{Name} =~ m/(?:dbname=)*(.*)/; my $directory = File::Basename::dirname($main_filename); my $schema_filename = File::Spec->catfile($directory, "${schema_name}.sqlite3"); unless (UR::Util::touch_file($schema_filename)) { Carp::carp("touch_file $schema_filename failed: $!"); return; } unless ($dbh->do(qq(ATTACH DATABASE '$schema_filename' as $schema_name))) { Carp::carp("Cannot attach file $schema_filename as $schema_name: ".$dbh->errstr); return; } } } sub attached_schemas { my($self, $dbh) = @_; $dbh ||= $self->get_default_handle; # Statement returns id, schema, filename my $sth = $dbh->prepare('PRAGMA database_list') || Carp::croak("Cannot list attached databases: ".$dbh->errstr); $sth->execute(); my %schemas = map { $_->[1] => $_->[2] } @{ $sth->fetchall_arrayref }; return \%schemas; } sub is_schema_attached { my($self, $schema, $dbh) = @_; $dbh ||= $self->get_default_handle; my $schemas = $self->attached_schemas($dbh); return exists $schemas->{$schema}; } 1; ValueDomain.pm100664023532023421 112012544604516 17517 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/DataSourcepackage UR::DataSource::ValueDomain; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::ValueDomain', is => ['UR::DataSource'], is_abstract => 1, properties => [ ], doc => 'A logical DBI-based database, independent of prod/dev/testing considerations or login details.', ); sub get_objects_for_rule { my $class = shift; my $rule = shift; my $obj = $UR::Context::current->_construct_object($rule); $obj->__signal_change__("define"); return $obj; } 1; Debug.pm100664023532023421 52212544604516 14274 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Debug; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; our $verify_indexes = 0; 1; =pod =head1 NAME UR::Debug - Controls for debugging behavior =head1 DESCRIPTION Flags which turn on debugging behavior are set here. Change them in the debugger to activate debugging functionality. =cut DeletedRef.pm100664023532023421 715212544604516 15277 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::DeletedRef; use strict; use warnings; require UR; BEGIN { # this is to workaround a Perl bug where the overload magic flag is not updated # for references with a different RV (which happens anytime you do "my $object" # https://rt.perl.org/rt3/Public/Bug/Display.html?id=9472 if ($^V lt v5.8.9) { eval "use overload fallback => 1"; }; }; our $VERSION = "0.44"; # UR $VERSION; our $all_objects_deleted = {}; sub bury { my $class = shift; for my $object (@_) { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM BURY object $object class ",$object->class," id ",$object->id,"\n"; } my $original_class = ref($object); my $original_id = $object->id; %$object = (original_class => ref($object), original_data => {%$object}); bless $object, 'UR::DeletedRef'; $all_objects_deleted->{$original_class}->{$original_id} = $object; Scalar::Util::weaken($all_objects_deleted->{$original_class}->{$original_id}); } return 1; } sub resurrect { shift unless (ref($_[0])); foreach my $object (@_) { my $original_class = $object->{'original_class'}; bless $object, $original_class; %$object = (%{$object->{original_data}}); my $id = $object->id; delete $all_objects_deleted->{$original_class}->{$id}; $object->resurrect_object if ($object->can('resurrect_object')); } return 1; } use Data::Dumper; sub AUTOLOAD { our $AUTOLOAD; my $method = $AUTOLOAD; $method =~ s/^.*:://g; Carp::croak("Attempt to use a reference to an object which has been deleted. A call was made to method '$method'\nRessurrect it first.\n" . Dumper($_[0])); } sub __rollback__ { return 1; } sub DESTROY { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY deletedref $_[0]\n"; } delete $all_objects_deleted->{"$_[0]"}; } 1; =pod =head1 NAME UR::DeletedRef - Represents an instance of a no-longer-existent object =head1 SYNOPSIS my $obj = Some::Class->get(123); $obj->delete; print ref($obj),"\n"; # prints 'UR::DeletedRef' $obj->some_method(); # generates an exception through Carp::confess $obj->resurrect; print ref($obj),"\n"; # prints 'Some::Class' =head1 DESCRIPTION Object instances become UR::DeletedRefs when some part of the application calls delete() or unload() on them, meaning that they no longer exist in that Context. The extant object reference is turned into a UR::DeletedRef so that if that same reference is used in any capacity later in the program, it will generate an exception through its AUTOLOAD to prevent using it by mistake. Note that UR::DeletedRef instances are different than Ghost objects. When a UR-based object is deleted through delete(), a new Ghost object reference is created from the data in the old object, and the old object reference is re-blessed as a UR::DeletedRef. Any variables still referencing the original object now hold a reference to this UR::DeletedRef. The Ghost object can be retrieved by issuing a get() against the Ghost class. Objects unloaded from the Context using unload(), or indirectly by rolling-back a transaction which triggers unload of objects loaded during the transaction, are also turned into UR::DeletedRefs. You aren't likely to encounter UR::DeletedRefs in normal use. What usually happens is that an object will be deleted with delete() (or unload()), the lexical variable pointing to the DeletedRef will soon go out of scope and the DeletedRef will then be garbage-colelcted. =head1 SEE ALSO UR::Object, UR::Object::Ghost, UR::Context =cut Pod2Html.pm100664023532023421 101612544604516 15423 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Docpackage UR::Doc::Pod2Html; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; use Data::Dumper; use parent 'Pod::Simple::HTML'; $Pod::Simple::HTML::Perldoc_URL_Prefix = ''; $Pod::Simple::HTML::Perldoc_URL_Postfix = '.html'; sub do_top_anchor { my ($self, $value) = @_; $self->{__do_top_anchor} = $value; } sub do_beginning { return 1; } sub do_end { return 1; } sub _add_top_anchor { my $self = shift; return $self->SUPER::_add_top_anchor(@_) if $self->{__do_top_anchor}; } 1; Section.pm100664023532023421 77712544604516 15373 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Docpackage UR::Doc::Section; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Doc::Section { is => 'UR::Object', has => [ title => { is => 'Text', is_optional => 1, }, content => { is => 'Text', doc => 'pod content for this section', }, format => { is => 'Text', default_value => 'pod', valid_values => ['html','pod','txt'], }, ], }; Writer.pm100664023532023421 133412544604516 15251 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Docpackage UR::Doc::Writer; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use Carp qw/croak/; class UR::Doc::Writer { is => 'UR::Object', is_abstract => 1, has => [ title => { is => 'Text', }, sections => { is => 'UR::Doc::Section', is_many => 1, }, navigation => { is => 'ARRAY', is_optional => 1, }, ], has_transient_optional => [ content => { is => 'Text', default_value => '', }, ] }; sub _append { my ($self, $data) = @_; $self->content($self->content . $data); } sub generate_index { my ($self, @command_trees) = @_; return ''; } Html.pm100664023532023421 561212544604516 16160 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Doc/Writerpackage UR::Doc::Writer::Html; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use UR::Doc::Section; use UR::Doc::Pod2Html; use Carp qw/croak/; class UR::Doc::Writer::Html { is => 'UR::Doc::Writer', }; sub render { my $self = shift; $self->content(''); $self->_render_header; $self->_render_index; my $i = 0; for my $section ($self->sections) { $self->_render_section($section, $i++); } $self->_render_footer; } sub _render_header { my $self = shift; if ($self->navigation) { my @nav_html; for my $item (@{$self->navigation}) { my ($name, $uri) = @$item; if ($uri) { push(@nav_html, "$name"); } else { push(@nav_html, $name); } } $self->_append(join(" :: ", @nav_html) . "
\n"); } my $translator = new UR::Doc::Pod2Html; my $title; $translator->output_string($title); $translator->parse_string_document("=pod\n\n".$self->title."\n\n=cut\n\n"); $self->_append("

$title

\n"); } sub _render_index { my $self = shift; my @titles = grep { $_ and /./ } map { $_->title } $self->sections; my $i = 0; if (@titles) { $self->_append("\n
    \n". join("\n", map {"
  • $_
  • "} @titles)."
\n\n"); } } sub _render_section { my ($self, $section, $idx) = @_; if (my $title = $section->title) { $self->_append("

$title

\n"); } my $content = $section->content; if ($section->format eq 'html') { $self->_append($content); } elsif ($section->format eq 'txt' or $section->format eq 'pod') { $content = "\n\n=pod\n\n$content\n\n=cut\n\n"; my $new_content; my $translator = new UR::Doc::Pod2Html; $translator->output_string($new_content); $translator->parse_string_document($content); $self->_append($new_content); } else { croak "Unknown section type " . $section->type; } $self->_append("
\n"); } sub _render_footer { my $self = shift; $self->_append(""); } sub generate_index { my ($self, @command_trees) = @_; return '' unless @command_trees; my $html = "

Command Index


\n"; $html .= $self->_generate_index_body(@command_trees); return $html; } sub _generate_index_body { my ($self, @command_trees) = @_; return '' unless @command_trees; my $html = "
    \n"; for my $tree (@command_trees) { my $name = $tree->{command_name_brief}; my $uri = $tree->{uri}; $html .= "
  • $name\n"; $html .= $self->_generate_index_body(@{$tree->{sub_commands}}); $html .= "
  • \n"; } $html .= "
\n"; return $html; } 1; Pod.pm100664023532023421 226712544604516 16001 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Doc/Writerpackage UR::Doc::Writer::Pod; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use UR::Doc::Section; use Carp qw/croak/; class UR::Doc::Writer::Pod { is => 'UR::Doc::Writer', }; sub render { my $self = shift; $self->content(''); $self->_render_header; $self->_render_index; map { $self->_render_section($_) } $self->sections; $self->_render_footer; return $self->content; } sub _render_header { my $self = shift; $self->_append("\n\n=pod\n\n"); if (my $title = $self->title) { $self->_append("=head1 $title\n\n"); } } sub _render_index { # no indexing for pod } sub _render_section { my ($self, $section) = @_; my $title = $section->title; $self->_append("=head1 $title\n") if $title; my $content = $section->content; if ($section->format eq 'html') { $self->warning_message("Skipping html section '$title' while rendering pod"); } elsif ($section->format eq 'txt' or $section->format eq 'pod') { $self->_append("\n\n=pod\n\n$content\n\n=cut\n\n"); } else{ croak "Unknown section type " . $section->type; } } sub _render_footer { my $self = shift; } 1; Env.pod100664023532023421 1211112544604516 14201 0ustar00abrummetgsc000000000000UR-0.44/lib/UR=pod =head1 NAME UR::Env - Environment variables that control UR behavior =head1 DESCRIPTION UR uses several environment variables to change its behavior or provide additional debugging information. =over 4 =item UR_STACK_DUMP_ON_DIE When true, has the effect of turning any die() into a Carp::confess, meaning a stack dump will be printed after the die message. =item UR_STACK_DUMP_ON_WARN When true, has the effect of turning any warn() into a Carp::cluck, meaning a stack dump will be printed after the warn message. =item UR_CONTEXT_ROOT The name of the Root context to instantiate when the program initializes. The default is UR::Context::DefaultRoot. Other Root Contexts can be used, for example, to connect to alternate databases when running in test mode. =item UR_CONTEXT_BASE This value only changes in a sub-process which goes to its parent process for object I/O instead of the root (which is the default value for the base context in an application). =item UR_CONTEXT_CACHE_SIZE_HIGHWATER Set the object count highwater mark for the object cache pruner. See also L =item UR_CONTEXT_CACHE_SIZE_LOWWATER Set the object count lowwater mark for the object cache pruner. See also L =item UR_DEBUG_OBJECT_RELEASE When true, messages will be printed to STDERR whenever objects are removed from the object cache, such as when the object pruner marks them for removal, when they are garbage collected, unloaded, or deleted. =item UR_DEBUG_OBJECT_RELEASE When true, messages will be printed to STDERR whenever the object pruner finishes its work, and show how many objects of each class were marked for removal. =item UR_CONTEXT_MONITOR_QUERY When true (non-zero), messages will be printed as the Context satisfies queries, such as when get() is called on a class, or while processing an iterator created through SomeClass->create_iterator and iterator->next(). If the value is 1, then only queries about Non-UR classes are printed. If 2, then all queries' information is printed. =item UR_DBI_MONITOR_SQL If this is true, most interactions with data sources such as connecting, disconnecting and querying will print messages to STDERR. Same as Cmonitor_sql()>. Note that this affects non-DBI data sources as well, such as file-based data sources, which will render file I/O information instead of SQL. =item UR_DBI_SUMMARIZE_SQL If true, a report will be printed to STDERR as the program finishes about what SQL queries have been done during the program's execution, and how many times they were executed. This is helpful during optimization. =item UR_DBI_MONITOR_EVERY_FETCH Used in conjunction with UR_DBI_MONITOR_SQL, tells the data sources to also print messages to STDERR for each row fetched from the underlying data source. Same as Cmonitor_every_fetch()>. =item UR_DBI_DUMP_STACK_ON_CONNECT Print a message to STDERR only when connecting to an underlying data source. Same as Cdump_stack_on_connect()> =item UR_DBI_EXPLAIN_SQL_MATCH If the query to a data source matches the given string (interpreted as a regex), then it will attempt to do an "explain plan" and print the results before executing the query. Same as Cexplain_sql_match()> =item UR_DBI_EXPLAIN_SQL_SLOW If the time between a prepare and the first fetch of a query is longer than the given number of seconds, then it will do an "explain plan" and print the results. Same as Cexplain_sql_slow()> =item UR_DBI_EXPLAIN_SQL_CALLSTACK Used in conjunction with UR_DBI_EXPLAIN_SQL_MATCH and UR_DBI_EXPLAIN_SQL_SLOW, prints a stack trace with Carp::longmess. Same as Cexplain_sql_callstack()> =item UR_DBI_MONITOR_DML Like UR_DBI_MONITOR_SQL, but only prints information during data-altering statements, like INSERT, UPDATE or DELETE. Same as Cmonitor_dml()> =item UR_DBI_NO_COMMIT If true, data source commits will be ignored. Note that saving still occurs. If you are working with a RDBMS database, this means During UR::Context->commit(), the insert, update and delete SQL statements will be issued, but the changes will not be committed. Useful for testing. Same as Cno_commit()> =item UR_USE_DUMMY_AUTOGENERATED_IDS If true, objects created without ID params will use a special algorithm to generate IDs. Objects with these special IDs will never be saved to a data source. Useful during testing. Same as Cuse_dummy_autogenerated_ids> =item UR_USED_LIBS If true, prints a message to STDERR with the contents of @INC just before the program exits. =item UR_USED_MODS If true, prints a message to STDERR with the keys of %INC just before the program exits. This will be a list of what modules had been loaded during the life of the program. If UR_USED_MODS is greater than 1, then it will show the key/value pairs of %INC, which will show the path each module was loaded from. =back =cut UR_COMMAND_DUMP_DEBUG_MESSAGES.pm100664023532023421 20012544604516 20475 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_COMMAND_DUMP_DEBUG_MESSAGES; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_COMMAND_DUMP_STATUS_MESSAGES.pm100664023532023421 20112544604516 20673 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_CONTEXT_BASE.pm100664023532023421 16112544604516 16321 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_BASE; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm100664023532023421 20112544604516 20641 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_CONTEXT_CACHE_SIZE_LOWWATER.pm100664023532023421 20012544604516 20562 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_CONTEXT_MONITOR_QUERY.pm100664023532023421 17212544604516 17725 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_MONITOR_QUERY; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_CONTEXT_ROOT.pm100664023532023421 16112544604516 16372 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_ROOT; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_DUMP_STACK_ON_CONNECT.pm100664023532023421 17612544604516 20326 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_CALLSTACK.pm100664023532023421 17612544604516 20267 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_IF.pm100664023532023421 16712544604516 17264 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_IF; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_MATCH.pm100664023532023421 17212544604516 17616 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_MATCH; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_SLOW.pm100664023532023421 17112544604516 17545 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_SLOW; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_MONITOR_DML.pm100664023532023421 16412544604516 16707 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_DML; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_MONITOR_EVERY_FETCH.pm100664023532023421 17412544604516 20077 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_EVERY_FETCH; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_MONITOR_SQL.pm100664023532023421 16412544604516 16732 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_SQL; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_NO_COMMIT.pm100664023532023421 16212544604516 16446 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_NO_COMMIT; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DBI_SUMMARIZE_SQL.pm100664023532023421 16612544604516 17161 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DBI_SUMMARIZE_SQL; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DEBUG_OBJECT_PRUNING.pm100664023532023421 17112544604516 17462 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DEBUG_OBJECT_PRUNING; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DEBUG_OBJECT_RELEASE.pm100664023532023421 17112544604516 17420 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DEBUG_OBJECT_RELEASE; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DUMP_DEBUG_MESSAGES.pm100664023532023421 17012544604516 17345 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DUMP_DEBUG_MESSAGES; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_DUMP_STATUS_MESSAGES.pm100664023532023421 17112544604516 17543 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_DUMP_STATUS_MESSAGES; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_IGNORE.pm100664023532023421 15312544604516 15367 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_IGNORE; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_NO_REQUIRE_USER_VERIFY.pm100664023532023421 17312544604516 20040 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_NO_REQUIRE_USER_VERIFY; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_NR_CPU.pm100664023532023421 15312544604516 15432 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_NR_CPU; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_RUN_LONG_TESTS.pm100664023532023421 16312544604516 16652 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_RUN_LONG_TESTS; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_STACK_DUMP_ON_DIE.pm100664023532023421 16612544604516 17157 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_STACK_DUMP_ON_DIE; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_STACK_DUMP_ON_WARN.pm100664023532023421 16712544604516 17326 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_STACK_DUMP_ON_WARN; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_TEST_QUIET.pm100664023532023421 15712544604516 16136 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_TEST_QUIET; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_USED_LIBS.pm100664023532023421 16112544604516 15754 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_CONTEXT_LIBS; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_USED_MODS.pm100664023532023421 15612544604516 15771 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_USED_MODS; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_USE_ANY.pm100664023532023421 15412544604516 15550 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_USE_ANY; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; UR_USE_DUMMY_AUTOGENERATED_IDS.pm100664023532023421 20012544604516 20552 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Envpackage UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; 1; Exit.pm100664023532023421 763412544604516 14212 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Exit; =pod =head1 NAME UR::Exit - methods to allow clean application exits. =head1 SYNOPSIS UR::Exit->exit_handler(\&mysub); UR::Exit->clean_exit($value); =head1 DESCRIPTION This module provides the ability to perform certain operations before an application exits. =cut # set up module require 5.006_000; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; our (@ISA, @EXPORT, @EXPORT_OK); require Exporter; @ISA = qw(Exporter); @EXPORT = qw(); @EXPORT_OK = qw(); use Carp; =pod =head1 METHODS These methods provide exit functionality. =over 4 =item exit_handler UR::Exit->exit_handler(\&mysub); Specifies that a given subroutine be run when the application exits. (Unimplimented!) =cut sub exit_handler { die "Unimplimented"; } =pod =item clean_exit UR::Exit->clean_exit($value); Exit the application, running all registered subroutines. (Unimplimented! Just exits the application directly.) =cut sub clean_exit { my $class = shift; my ($value) = @_; $value = 0 unless defined($value); exit($value); } =pod =item death Catch any die or warn calls. This is a universal place to catch die and warn if debugging. =cut sub death { unless ($ENV{'UR_STACK_DUMP_ON_DIE'}) { return; } # workaround common error if ($_[0] =~ /Can.*t upgrade that kind of scalar during global destruction/) { exit 1; } if (defined $^S) { # $^S is defined when perl is executing (as opposed to interpreting) if ($^S) { # $^S is true when its executing in an eval, false outside of one return; } } else { # interpreter is parsing a module or string eval # check the call stack depth for up-stream evals # fall back to perls default handler if there is one my $call_stack_depth = 0; for (1) { my @details = caller($call_stack_depth); #print Data::Dumper::Dumper(\@details); last if scalar(@details) == 0; if ($details[1] =~ /\(eval .*\)/) { #print ""; return; } elsif ($details[3] eq "(eval)") { #print ""; return; } $call_stack_depth++; redo; } } if ( $_[0] =~ /\n$/ and UNIVERSAL::can("UR::Context::Process","is_initialized") and defined(UR::Context::Process->is_initialized) and (UR::Context::Process->is_initialized == 1) ) { # Do normal death if there is a newline at the end, and all other # things are sane. return; } else { # Dump the call stack in other cases. # This is a developer error occurring while things are # initializing. local $Carp::CarpLevel = 1; Carp::confess(@_); return; } } =pod =item warning Give more informative warnings. =cut sub warning { unless ($ENV{'UR_STACK_DUMP_ON_WARN'}) { warn @_; return; } return if $_[0] =~ /Attempt to free unreferenced scalar/; return if $_[0] =~ /Use of uninitialized value in exit at/; return if $_[0] =~ /Use of uninitialized value in subroutine entry at/; return if $_[0] =~ /One or more DATA sections were not processed by Inline/; UR::ModuleBase->warning_message(@_); if ($_[0] =~ /Deep recursion on subroutine/) { print STDERR "Forced exit by UR::Exit on deep recursion.\n"; print STDERR Carp::longmess("Stack tail:"); exit 1; } return; } #$SIG{__DIE__} = \&death unless ($SIG{__DIE__}); #$SIG{__WARN__} = \&warning unless ($SIG{__WARN__}); sub enable_hooks_for_warn_and_die { $SIG{__DIE__} = \&death; $SIG{__WARN__} = \&warning; } &enable_hooks_for_warn_and_die(); 1; __END__ =pod =back =head1 SEE ALSO UR(3), Carp(3) =cut #$Header$ Manual.pod100664023532023421 160112544604516 14650 0ustar00abrummetgsc000000000000UR-0.44/lib/UR=pod =head1 NAME UR::Manual - Short list of UR's documentation =head1 Manuals L - Short introduction L - UR from Ten Thousand Feet L - Getting started with UR L - A few things to keep in mind when designing a database schema L - Slides for a presentation on UR L - Recepies for getting stuff working L - UR's metadata system L - Defining classes L - UR's command line tool =head1 Basic Entities L - Pretty much everything is-a UR::Object L - Metadata class for Classes L - Metadata class for Properties L - Manage packages and classes L - Software transactions and More! L - How and where to get data Cookbook.pod100664023532023421 2743612544604516 16454 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::Cookbook - Recepies for getting things working =head1 Database Changes =head2 Synchronizing your classes to the database schema From under your application's Namespace directory, use the command-line tool ur update classes This will load all the data sources under the DataSource subdirectory of the Namespace, find out what has changed between the last time you ran update classes (possibly never) and now, save the current database schema information in the Namespace's MetaDB, and update the class definitions for any changed entities. =head2 Possible conflicts Avoid tables called 'type' or 'types'. It will conflict with the class metadata class names where their class names end in '::Type'. The 'ur update classes' tool will rename the class to 'YourNamespace::TypeTable' to avoid the conflict, while keeping the table_name the same. A table with multiple primary keys should not have one of them called 'id'. This will result in a conflict with the requirement that a class must have have a property called 'id' that uniquely identifies a member. =head1 Relationships Class relationships provide a way to describe how one class links to another. They are added to a class by creating a property that lists how the class' properties relate to each other. There are two basic kinds of relationships: forward and reverse, Forward relationships are used to model the has-a condition, where the primary class holds the ID of the related class's instance. Reverse relationships are used when the related class has a property pointing back to the primary class. They are usually used to model a has-many situation where the related class holds the ID of which primary class instance it is related to. =head2 Has-a (One-to-one) The container class/table has a foreign key pointing to a contained class/table as in table Container column type constraint ---------------------------------------- container_id Integer primary key value Varchar not null contained_id Integer references contained(contained_id) table Contained column type constraint ---------------------------------------- contained_id Integer primary key contained_value Varchar not null Adding a forward relationship involves creating a property where the 'is' is the name of the related class, and an 'id_by' indicating which property on the primary class provides the foreign key with the related class' ID. The class definition for the container would look like this: class TheNamespace::Container { table_name => 'container', id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, ], has_optional => [ contained_id => { is => 'Integer' }, contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; If there was a NOT NULL constraint on the contained_id column, then the contained_id and contained properties should go in the "has" section. And now for the contained class. We'll also include a reverse relationship pointing back to the container it's a part of. class TheNamespace::Contained { table_name => 'contained', id_by => [ contained_id => { is => 'Integer' }, ], has => [ container => { is => 'TheNamespace::Container', reverse_as => 'contained', is_many => 1 }, contained_value => { is => 'Varchar' }, ], data_source => 'TheNamsapce::DataSource::TheDatabase', }; Note that the reverse_as parameter of the container property actually points to the object accessor, not the id accessor. It doesn't make sense, but that's how it is for now. Hopefully we'll come up with a better syntax. =head2 Has-many The contained class/table has a foreign key pointing to the container it's a part of. table Container column type constraint ------------------------------------------ container_id Integer primary key value Varchar not null table Contained column type constraint ------------------------------------------ contained_id Integer primary key contained_value Varchar not null container_id Integer references container(container_id) To create a reverse relationship, you must first create a forward relationship on the related class pointing back to the primary class. Then, creating the reverse relationship involves adding a property where the 'is' is the name of the related class, and a 'reverse_as' indicating which property on the related class describes the forward relationship between that related class and the primary class. class TheNamespace::Container { table_name => 'container', id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, containeds => { is => 'TheNamespace::Contained', reverse_as => 'container', is_many => 1 }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Contained { table_name => 'contained', id_by => [ contained_id => { is => 'Integer' }, ], has => [ contained_value => { is => 'Varchar' }, container_id => { is => 'Integer' }, container => { is => 'TheNamespace::Container', id_by => 'container_id' }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Many-to-many Storing a has-many relationship requires a bridge table between the two main entities. table Container column type constraint -------------------------------------------- container_id Integer primary key value Varchar not null table Contained column type constraint -------------------------------------------- contained_id Integer primary key contained_value Varchar not null container_id Integer references container(container_id) table Bridge column type constraint -------------------------------------------- container_id Integer references container(container_id) contained_id Integer references contained(contained_id) primary key(container_id,contained_id) Here, both the Container and Contained classes have accessors to return a list of all the objects satisfying the relationship through the bridge table. class TheNamespace::Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, ], has_many => [ bridges => { is => 'TheNamespace::Bridge', reverse_as => 'container' }, containeds => { is => 'TheNamespace::Contained', via => 'bridge', to => 'contained' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Bridge { id_by => [ container_id => { is => 'Integer' }, contained_id => { is => 'Integer' }, ], has => [ container => { is => 'TheNamespace::Container', id_by => 'container_id' }, contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, ], table_name => 'bridge', data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Contained { id_by => [ container_id => { is => 'Integer' }, ], has => [ contained_value => { is => 'Varchar' }, ], has_many => [ bridges => { is => 'TheNamespace::Bridge', reverse_as => 'contained' }, containers => { is => 'TheNamespace::Container', via => 'bridge', to => 'container' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head1 Indirect Properties Indirect properties are used to add a property to a class where the data is actually stored in a direct property of a related class. =head2 Singly-indirect As in the has-a relationship, and the container class wants to have a property actually stored on the contained class. Using the same schema in the has-a relationship above, and we want the contained_value property to be accessible from the container class. class TheNamespace::Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ # This implies a contained_id property, too contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, contained_value => { via => 'contained', to => 'contained_value' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; You can now use C as an accessor on TheNamespace::Container objects. You can also use C as a parameter in C, and the underlying data source will use a join if possible in the SQL query. =head2 Many Singly-indirect As in the singly-indirect recipe, but the container-contained relationship is has-many class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ containeds => { is => 'TheNamespace::Contained', reverse_as => 'container', is_many => 1 }, contained_values => { via => 'containeds', to => 'container_value', is_many => 1 }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Doubly-indirect If you have a normal has-a relationship between a container and a contained item, and the contained item also has-a third-level contained thing, and you'd like to have a property of the innermost class available to the first container: class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ contained => { is => 'TheNamsepace::Contained', id_by => 'contained_id '}, inner_contained => { is => 'TheNamespace::InnerContained, via => 'contained', to => 'inner_contained_id' }, inner_contained_value => { via => 'inner_contained', to => 'inner_contained_value' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Many doubly-indirect Combining the has-many relationship and the doubly indirect recipe class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ containeds => { is => 'TheNamsepace::Contained', reverse_as => 'container', is_many => 1}, inner_containeds => { is => 'TheNamespace::InnerContained, via => 'contained', to => 'contained', is_many => 1 }, inner_contained_values => { via => 'inner_containeds', to => 'inner_contained_value', is_many => 1 }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; And then you get an accessor inner_containeds to return a list of inner-contained objects, and another accessor inner_contained_values to return a list of their values. Metadata.pod100664023532023421 706612544604516 16403 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::Metadata - Overview of the metadata classes in UR =head1 SYNOPSIS use MyNamespace; my $class_meta = MyNamespace::SomeClass->__meta__; my @property_metas = $class_meta->direct_property_metas(); my @parent_class_metas = $class_meta->ancestry_class_metas(); my $table_obj = UR::DataSource::RDBMS::Table->get( table_name => $class_meta->table_name, ); my @column_objs = $table_obj->columns(); =head1 DESCRIPTION The UR system creates and uses several classes and objects to represent information about the many classes and objects in the system - metadata. For example, for each class, there is an object, called a class metadata object, to represent it. Each property in a class has metadata. So does the relationship between parent and child classes and relationships involved in delegated properties. metadata about any database schemas your namespace knows about is also tracked and stored. These classes define an API for introspection and reflection, a way for the system to change itself as it runs, and methods for tracking changes and applying those changes to files and databases. =head1 APIs The metadata API is divided into 5 primary parts: =over 4 =item Defining Classes The mechanism for defining class structure, including their properties and relationships. It handles creating accessor/mutator methods for you. The syntax for defining classes is detailed in the L page. =item Objects Representing Classes, Properties, Relationships, etc. UR Classes aren't just conceptual entities like a package name, they have object instances to represent them. For every named class, you can get a L instance with that C. Each property defined on that class has a L with a matching C and C pair. Even those basic metadata classes have class, property and relationship metadata of their own. =item Schema Objects If you use the C command-line tool to manage the linkage between your database schema(s) and class structure (it's not necessary; you can also manage it by hand), then objects will also exist to represent the database entities. See also L =over 2 =item . tables L =item . columns L =item . Foreign key constraints L and L =item . Primary key constraints L =item . Unique constraints L =back =item Namespaces, Contexts and Data Sources Namespaces (L) collect and manage groups of related classes. Classes can be a member of one Namespace, and in practice will live in a subdirectory under the Namespace module's name. Contexts (L) and Data Sources (L) provide a filtered view of the data that is reachable through the current Namespace. =item Index, Change, Observer and other incidentals And then there's everything else L objects are created by the system to handle get() requests for non-ID parameters. L objects represent a change in the system during a software transaction, such as an object's property changind value or creating a new instance of something. L objects manage the change subscription system, where the application can be notified of changes through callbacks. See also L. =back Overview.pod100664023532023421 2447212544604516 16511 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::Overview - UR from Ten Thousand Feet =head1 Perspective on Objects Standard software languages provide a facility for making objects. Those objects have certain characteristics which are different with UR objects. A standard object in most languages: =over 4 =item * exists only as long as the program which created it has a reference to it =item * requires that the developer manage organizing the object(s) into a structure to support any searching required =item * handles persistence between processes explicitly, by saving or loading the object to external storage =item * references other objects only if explicitly linked to those objects =item * acts as a functional software device, but any meaning associated with the object is implied by how it is used =back Regular objects like those described above are the building blocks of most software. In many cases, however, they are often used for a second, higher-level purpose: defining entities in the domain model of the problem area the software addresses. UR objects are tailored to represent domain model entities well. In some sense, UR objects follow many of the design principles present in relational databases, and as such mapping to a database for UR objects is trivial, and can be done in complex ways. UR objects differ from a standard object in the following key ways: =over 4 =item * the object exists after creation until explicitly deleted, or the transaction it is in rolled-back =item * managing loaded objects is done automatically by a Context object, which handles queries, saving, lazy-loading and caching =item * it is possible to query for an object by specifying the class and the matching characteristics =item * the object can reference other objects which are not loaded in the current process, and be referenced by objects not in the current process =item * the object is a particular truth-assertion in the context in which it exists =back =head1 Object-Relational Mapping UR's primary reason for existing is to function as an ORM. That is, managing how to store instances of objects in memory of a running program with more persistent storage in a relational database, and retrieve them later. It handles the common cases where each table is implemented by a class their columns are properties of the classes; retrieving objects by arbitrary properties; creating, updating and deleting objects with enforced database constraints; and named relationships between classes. It can also handle more complicated things like: =over 4 =item * classes for things which are not database entities at all =item * derived classes where the data spans multiple tables between the parent and child classes =item * loading an object through a parent class and having it automatically reblessed into the appropriate subclass =item * properties with no DB column behind them =item * calculated properties with a formula behind them =item * inheritance hierarchies that may have tables missing at some or all stages =item * meta-data about Properties, Classes and the relationships between them =back =head1 Object Context With UR, every object you create is made a part of the current "Context". Conceptually, the Context is the lens by which your application views the data that exists in the world. At one level, you can think of the current context as an in-memory transaction. All changes to the object are tracked by the context. The Context knows how to map objects to their storage locations, called Data Sources. Saving your changes is simply a matter of asking the current context to commit. The Context can also reverse the saving process, and map a request for an object to a query of external storage. Requests for objects go through the Context, are loaded from outside as needed, and are returned to the caller after being made part of the current context's transaction. Objects never reference each other by actual Perl reference internally, instead they use the referent's ID. Accessors on an object which return another object send the ID through the context to get the object back, allowing the context to load the referenced object only when it is actually needed. This means that your objects can hook together until references span an entire database schema, and pulling one object from the database will not load the entire database into memory. The context handles caching, and by default will cache everything it touches. This means that you can ask for the same thing multiple times, and only the first request will actually hit the underlying database. It also means that requests for objects which map to the same ID will return the exact same instance of the object. The net effect is that each process's context is an in-memory database. All object creation, deletion, and change is occurring directly to that database. For objects configured to have external persistence, this database manages itself as a "diff" vs. the external database, allowing it to simulate representing all UR data everywhere, while only actually tracking what is needed. =head2 Benefits =over 4 =item * database queries don't repeat themselves again and again =item * you never write insert/update/delete statements, or work out constraint order yourself =item * allows you to write methods which address an object individually, with ways to avoid many individual database queries =item * explicitly clearing the cache is less complex than explicitly managing the caching of data =back =head2 Issues =over 4 =item * the cache grows until you explicitly clear it =item * there is CPU overhead checking the cache if you really are always going directly to the database =back Unloading objects from the cache can be done in several ways =over 4 =item * Calling unload() as a class or instance method to unload all objects of a class, or one particular object. =item * Setting object count limits explicitly with object_cache_size_lowwater() and object_cache_size_highwater() in L =item * Creating a L instance. When it goes out of scope, all UR Objects loaded during its lifetime will be unloaded. =back =head1 Class Definitions At the top of every module implementing a UR class is a block of code that defines the class to explicitly spell out its inheritance, properties and types, constraints, relationships to other classes and where the persistent storage is located. It's meant to be easy to read and edit, if necessary. If the class is backed by a database table, then it can also maintain itself. =head1 Metadata Besides the object instances representing data used by the program, the UR system has other objects representing metadata about the classes (class information, properties, relationships, etc), database entities (databases, tables, columns, constraints, etc), transactions, data sources, etc. All the metadata is accessible through the same API as any of the database-backed data. For classes backed by the database, after a schema change (like adding tables or columns, altering types or constraints), a command-line tool can automatically detect the change and alter the class definition in the Perl module to keep the metadata in sync with the database. =head1 Documentation System At the simplest level, most entities have a 'doc' metadata attribute to attach some kind of documentation to. There's also a set of tools that can be run from the command line or a web browser to view the documentation. It can also be used to browse through the class and database metadata, and generate diagrams about the metadata. =head1 Iterators If a retrieval from the database is likely to result in the generation of tons of objects, you can choose to get them back in a list and keep them all in memory, or get back a special Iterator object that the program can use to get back objects in batches. =head1 Command Line Tools UR has a central command-line tool that cam be used to manipulate the metadata in different ways. Setting up namespaces, creating data sources, syncing classes with schemas, accessing documentation, etc. There is also a framework for creating classes that represent command line tools, their parameters and results, and makes it easy to create tools through the Command Pattern. =head1 Example Given these classes: =over 4 =item PathThing/Path.pm use strict; use warnings; use PathThing; # The application's UR::Namespace module class PathThing::Path { id_by => 'path_id', has => [ desc => { is => 'String' }, length => { is => 'Integer' }, ], data_source => 'PathThing::DataSource::TheDB', table_name => 'PATHS', }; =item PathThing/Node.pm class PathThing::Node { id_by => 'node_id', has => [ left_path => { is => 'PathThing::Path', id_by => 'left_path_id' }, left_path_desc => { via => 'left_path', to => 'desc' }, left_path_length => { via => 'left_path', to => 'length' }, right_path => { is => 'PathThing::Path', id_by => 'right_path_id' }, right_path_desc => { via => 'right_path', to => 'desc' }, right_path_length => { via => 'right_path', to => 'length' }, map_coord_x => { is => 'Integer' }, map_coord_y => { is => 'String' }, ], data_source => 'PathThing::DataSource::TheDB', table_name => 'NODES', }; =back For a script like this one: use PathThing::Node; my @results = PathThing::Node->get( right_path_desc => 'over the river', left_path_desc => 'through the woods', right_path_length => 10, ); It will generate SQL like this: select NODES.NODE_ID, NODES.LEFT_PATH_ID, NODES.RIGHT_PATH_ID, NODES.MAP_COORD_X, NODES.MAP_COORD_Y, left_path_1.PATH_ID, left_path_1.DESC, left_path_1.LENGTH right_path_1.PATH_ID, right_path_1.DESC, right_path_1.LENGTH from NODES join PATHS left_path_1 on NODES.LEFT_PATH_ID = left_path_1.PATH_ID join PATHS right_path_1 on NODES.RIGHT_PATH_ID = right_path1.PATH_ID where left_path_1.DESC = 'through the woods' and right_path_1.DESC = 'over the river', and right_path_1.LENGTH = 10 And for every row returned by the query, a PathThing::Node and two PathThing::Path objects will be instantiated and stored in the Context's cache. C<@results> will contain a list of matching PathThing::Node objects. Presentation.pod100664023532023421 67612544604516 17316 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::Presentation - Slides for a presenation on UR =head1 Overview UR_Presentation.pdf is located in the Manual/ subdirectory of the UR distribution. It contains slides for a presentation on UR originally given at the Lambda Lounge meeting in St. Louis, MO on May 6 2009. The presentation covers the implementation of an over-the-top vending machine that stores its contents and inventory in an SQLite database. SchemaDesign.pod100664023532023421 237412544604516 17212 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::SchemaDesign - Tips for designing an efficient schema for UR =head1 Relational Databases =over 4 =item Avoid creating a table called 'type' or 'types'. When 'ur update classes' translates it into a class name, it will become YourNamespace::Type. Class names ending in '::Type' are reserved for class metadata, the class will be renamed to 'YourNamespace::TypeTable' to avoid the conflict. The table_name for that class will still refer to the actual table name. 'ur update classes' will print a warning if this happens, and rename the class automatically. =item Avoid columns named 'id' UR expects an object to be uniquely identified by a property called 'id'. Classes cannot have multiple ID properties where one of them is called 'id', because 'id' would no uniqiely identify one of them. If you want to call the column 'id', then the property name in the class metadata must be something else ('id_id', for example) in both the 'has' and 'id_by' sections, and the column_name set to 'id'. =item Indexes for common queries Create indexes in your database to cover common queries. If you routinely make queries involving non-primary keys, creating an index that includes these other columns will improve query times. =back =pod Tutorial.pod100664023532023421 3305612544604516 16504 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual=pod =head1 NAME UR::Manual::Tutorial - Step-by-step guide to building a set of classes for a simple database schema =head1 Overview We'll use the familiar "Music Database" example used in many ORM tutorials: Our database has the following basic entities and relationships: =over 2 =item * One artist can have many CDs =item * One CD belongs to one artist =item * one CD can have many tracks =item * one track belongs to one CD =back =head1 The "ur" command-line program The tool for working with UR from the command line is 'ur' . It is installed with the UR module suite. Just type "ur" and hit enter, to see a list of valid ur commands: > ur Sub-commands for ur: init NAMESPACE [DB] initialize a new UR app in one command define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging The "ur" command works a lot like the "svn" command: it is the entry point for a list of other subordinate commands. =over 2 =item * Typing something like "ur browser" will run the browser tool. =item * Typing something like "ur define" will give another list, of even more granular commands which are under "ur define": =back > ur define Sub-commands for ur define: namespace NSNAME create a new namespace tree and top-level module db URI NAME add a data source to the current namespace class --extends=? [NAMES] Add one or more classes to the current namespace At any point, you can put '--help' as a command line argument and get some (hopefully) helpful documentation. In many cases, the output also resembles svn's output where the first column is a character like 'A' to represent something being added, 'D' for deleted, etc. (NOTE: The "ur" command, uses the Command API, an API for objects which follow the command-pattern. See L for more details on writing tools like this. =head1 Define a UR Namespace A UR namespace is the top-level object that represents your data's class structure in the most general way. For this new project, we'll need to create a new namespace, perhaps within a testing directory. ur define namespace Music And you should see output like this: A Music (UR::Namespace) A Music::Vocabulary (UR::Vocabulary) A Music::DataSource::Meta (UR::DataSource::Meta) A Music/DataSource/Meta.sqlite3-dump (Metadata DB skeleton) showing that it created 3 classes for you, Music, Music::Vocabulary and Music::DataSource::Meta, and shows what classes those inherit from. In addition, it has also created a file to hold your metadata. Other parts of the documentation give a more thorough description of Vocabulary and Metadata classes. =head1 Define a Data Source A UR DataSource is an object representing the location of your data. It's roughly analogous to a Schema class in DBIx::Class, or the "Base class" in Class::DBI. Note: Because UR can be used with objects which do NOT live in a database, using a data source is optional, but is the most common case. Most ur commands operate in the context of a Namespace, including the one to create a datasource, so you need to be within the Music's Namespace's directory: cd Music and then define the datasource. We specify the data source's type as a sub-command, and the name with the --dsname argument. For this example, we'll use a brand new SQLite database. For some other, perhaps already existing database, give its connect string instead. ur define db dbi:SQLite:/var/lib/music.sqlite3 Example which generates this output: A Music::DataSource::Example (UR::DataSource::SQLite,UR::Singleton) ...connecting... ....ok and creates a symlink to the database at: Music/DataSource/Example.sqlite3 and shows that it created a class for your data source called Music::DataSource::Example, which inherits from UR::DataSource::SQLite. It also created an empty database file and connected to it to confirm that everything is OK. =head1 Create the database tables Here are the table creation statements for our example database. Put them into a file with your favorite editor and call it example-db.schema.txt: CREATE TABLE artist ( artist_id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE cd ( cd_id INTEGER NOT NULL PRIMARY KEY, artist_id INTEGER NOT NULL CONSTRAINT CD_ARTIST_FK REFERENCES artist(artist_id), title TEXT NOT NULL, year INTEGER ); CREATE TABLE track ( track_id INTEGER NOT NULL PRIMARY KEY, cd_id INTEGER NOT NULL CONSTRAINT TRACK_CD_FK REFERENCES cd(cd_id), title TEXT NOT NULL ); This new SQLite data source assumes the database file will have the pathname Music/DataSource/Example.sqlite3. You can populate the database schema like this: sqlite3 DataSource/Example.sqlite3 < example-db.schema.txt =head1 Create your data classes Now we're ready to create the classes that will store your data in the database. You could write those classes by hand, but it's easiest to start with an autogenerated group built from the database schema: ur update classes-from-db is the command that performs all the magic. You'll see it go through several steps: =over 2 =item 1. Find all the defined datasources within the current namespace =item 2. Query the data sources about what tables, columns, constraints and foreign keys are present =item 3. Load up all the classes in the current namespace =item 4. Figure out what the differences are between the database schema and the class structure =item 5. Alter the class metadata to match the database schema =item 6. Use the new class metadata to write headers on the Perl module files in the namespace =back There will now be a Perl module for each database table. For example, in Cd.pm: package Music::Cd; use strict; use warnings; use Music; class Music::Cd { table_name => 'CD', id_by => [ cd_id => { is => 'INTEGER' }, ], has => [ artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' }, artist_id => { is => 'INTEGER' }, title => { is => 'TEXT' }, year => { is => 'INTEGER', is_optional => 1 }, ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; 1; The first few lines are what you would see in any Perl module. The keyword C tells the UR system to define a new class, and lists the properties of the new class. Some of the important parts are that instances of this class come from the Music::DataSource::Example datasource, in the table 'CD'. This class has 4 direct properties (cd_id, artist_id, title and year), and one indirect property (artist). Instances are identified by the cd_id property. Methods are automatically created to match the property names. If you have an instance of a CD, say $cd, you can get the value of the title with C<$cd-Etitle>. To get back the artist object that is related to that CD, C<$cd-Eartist>. =head1 CRUD (Create, Read, Update, Delete) =head2 Create Creating new object instances is done with the create method; its arguments are key-value pairs of properties and their values. #!/usr/bin/perl use strict; use Music; my $obj1 = Music::Artist->create(name => 'Elvis'); my $obj2 = Music::Artist->create(name => 'The Beatles'); UR::Context->commit(); And that's it. After this script runs, there will be 2 rows in the Artist table. Just a short aside about that last line... All the changes to your objects while the program runs (creates, updates, deletes) exist only in memory. The current "Context" manages that knowledge. Those changes are finally pushed out to the underlying data sources with that last line. =head2 Read Retrieving object instances from the database is done with the C method. A C with no arguments will return a list of all the objects in the table. @all_cds = Music::Cd->get(); If you know the "id" (primary key) value of the objects you're interested in, you can pass that "id" value as a single argument to get: $cd = Music::Cd->get(3); An arrayref of identity values can be passed-in as well. Note that if you query is going to return more than one item, and it is called in scalar context, it will generate an exception. @some_cds = Music::Cd->get([1, 2, 4]); To filter the return list by a property other than the ID property, give a list of key-value pairs: @some_cds = Music::Cd->get(artist_id => 3); This will return all the CDs with the artist ID 5, 6 or 10. @some_cds = Music::Cd->get(artist_id => [5, 6, 10]); get() filters support operators other than strict equality. This will return a list of CDs with artist ID 2 and have the word 'Ticket' somewhere in the title. @some_cds = Music::Cd->get(artist_id=> 2, title => { operator => 'like', value => '%Ticket%'} ); To search for NULL fields, use undef as the value: @cds_with_no_year = Music::Cd->get(year => undef); =head2 get_or_create C is used to retrieve an instance from the database if it exists, or create a new one if it does not. $possibly_new = Music::Artist->get_or_create(name => 'The Band'); =head2 Update All the properties of an object are also mutators. To change the object's property, just call the method for that property with the new value. $cd->year(1990); Remember that any changes made while the program runs are not saved in the database until you commit the changes with Ccommit>. =head2 Delete The C method does just what it says. @all_tracks = Music::Track->get(); foreach my $track ( @all_tracks ) { $track->delete(); } Again, the corresponding database rows will not be removed until you commit. =head1 Relationships After running ur update classes, it will automatically create indirect properties for all the foreign keys defined in the schema, but not for the reverse relationships. You can add other relationships in yourself and they will persist even after you run ur update classes again. For example, there is a foreign key that forces a track to be related to one CD. If you edit the file Cd.pm, you can define a relationship so that CDs can have many tracks: class Music::Cd { table_name => 'CD', id_by => [ cd_id => { is => 'INTEGER' }, ], has => [ artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' }, artist_id => { is => 'INTEGER' }, title => { is => 'TEXT' }, year => { is => 'INTEGER' }, tracks => { is => 'Music::Track', reverse_as => 'cd', is_many => 1 }, # This is the new line ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; This tells the system that there is a new property called 'tracks' which returns items of the class Music::Track. It links them to the acting CD object through the Track's cd property. After that is in place, you can ask for a list of all the tracks belonging to a CD with the line @tracks = $cd->tracks() You can also define indirect relationships through other indirect relationships. For example, if you edit Artist.pm to add a couple of lines: class Music::Artist { table_name => 'ARTIST', id_by => [ artist_id => { is => 'INTEGER' }, ], has => [ name => { is => 'TEXT' }, cds => { is => 'Music::Cd', reverse_as => 'artist', is_many => 1 }, tracks => { is => 'Music::Track', via => 'cds', to => 'tracks', is_many => 1}, ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; This defines a relationship 'cds' to return all the CDs from the acting artist. It also defines a relationship called 'tracks' that will, behind the scenes, first look up all the CDs from the acting artist, and then find and return all the tracks from those CDs. Additional arguments can be passed to these indirect accessors to get a subset of the data @cds_in_1990s = $artist->cds(year => { operator => 'between', value => [1990,1999] } ); would get all the CDs from that artist where the year is between 1990 and 1999, inclusive. Note that is_many relationships should always be named with plural words. The system will auto-create other accessors based on the singular name for adding and removing items in the relationship. For example: $artist->add_cd(year => 1998, title => 'Cool Jams' ); would create a new Music::Cd object with the given year and title. The cd_id will be autogenerated by the system, and the artist_id will be automatically set to the artist_id of $artist. =head1 Custom SQL It's possible to use get() with custom SQL to retrieve objects, as long as the select clause includes all the ID properties of the class. To find Artist objects that have no CDs, you might do this: my @artists_with_no_cds = Music::Artist->get(sql => 'select artist.artist_id, count(cd.artist_id) from artist left join cd on cd.artist_id = artist.artist_id group by artist.artist_id having count(cd.artist_id) = 0' ); UR_Presentation.pdf100664023532023421 237217712544604516 20025 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Manual%PDF-1.3 %Äåòåë§ó ÐÄÆ 4 0 obj << /Length 5 0 R /Filter /FlateDecode >> stream x1OÃ0…÷üŠ7&C\ŸåØÉ­ A*WC‰LDš¤þ{œÚ „ð öÝÙþî½rìÀ}(Й@k±†ÃlÑŠnlqtÅp‹ =¬ô³xÈ´ÈEPÔ˜”c{ØE’1˜8LH˜³Kbó„ДWÖ5µÅº޶Ì3. òà/~²H§ÌO)ÍÒ#,a÷×üäá¦++·í‡•«ÞmÛUý>ÂÌõˆþL)$K=YfãGò$óζ/³Õ·Ë›ƒÀ‘B\’Rz2ó+ûÕLâI2|'‰Ì˜Vßìô Ï]_6nyûV×¶ïÇétV¼nkÚ( …p{ª7ÿ¼*wˆ endstream endobj 5 0 obj 269 endobj 2 0 obj << /Type /Page /Parent 3 0 R /Resources 6 0 R /Contents 4 0 R /MediaBox [0 0 612 792] >> endobj 6 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> >> endobj 9 0 obj << /Length 10 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >> stream x…”MHaÇÿ³±Ñ—ÅÐÁ$T& RÓõ+S¶eÕL b}wg§™Ý-E"„è˜uŒ.VD‡ˆNá¡C§:D™u‰ £E^"¶ÿ;“»cT¾03¿yžÿû|½ÃURŽcE4`ÊλÉÞ˜vztLÛüU¨F\)Ãs:‰Ÿ©•Ïõkõ-iYj”±Öû6|«v™P4*wd>,y<àã’/ä<5g$©4Ù!7¸CÉNò-òÖlˆÇCœžTµS“3—q";È-E#+c> ëvÚ´Éï¥=íSÔ°ßÈ79 Ú¸òý@Û`Ó‹ŠmÌÜv×Ulõ5ÀÎ`ñPÅö=éÏGÙõÊËjöÃ)ÑkúP*}¯6ß~^/•~Ü.•~ÞaÖñÔ2 nÑײ0å%Ôìfüäý‹ƒž|U °À9Žlú¯7?ûÛ‰j`¨‘Ël7¸òâ"çtæœi×ÌNäµf]?¢uðh…ÖgM Zʲ4ßåi®ð„[é&LYÎÙ_Ûx {xOö¹$¼î̥߬S]œ%šØÖ§´èê&7ïgÌž>r=¯÷·g8`候ï 8rʶâ<©‰ÔØãñ“dÆWT'“ó<çeLß~.u"A®¥=9™ë—š]ÜÛ>31Ä3’¬X3ñßüÆ-$eÞ}ÔÜu,ÿ›gm‘g…6ï64$Ñ‹áÀEzL*LZ¥_ÐjÂÃä_•å]½XážÏy¸[Æ?…Xs åšþNÿ¢/ë ú]ýó|m¡¾â™sϚƫk_Wf–ÕȸA2¾¬)ˆo°Úz-diâôä•õáê2ö|mÙ£Éâj|5Ô¥ejÄ8ãÉ®e÷E²Å7áç[Ëö¯éQû|öIM%ײºxf)ú|6\ kÿ³«`Ò²«ðä.> stream xµUQo›0~çWÜžšH©‹ !d{[·J›Ôiݘú°îÁ!¡Lm³*ýõ;p‚Ùš‡¬M)È8ùî>ß÷Ý=À <€Ÿˆ2˜Í(·PÁÅ¥¦êîȶ¿"l֮ؾìw3ü£Ï¼´„÷ Ð0ìŽÛçÜ'Aç1‰‚9.HJ¸¸bÄ É ~Âèv 4‚QnÖpÖh?¾½;ƒl †2ö~Aò>&6IBÙÔǵ ¿ÛÙðð< Ô†½CrÿbpE$„!2Ý}ªx¸Ë<Šç$:Iæd—ù¥,K^-¡È+FÊBã7à+ž‰¶0 Jaø’~8¿=Âpü¨:yyaö`o 〻k‰â^IUòÒ‚k KOÅM.+=ö^!‡£`“ð$%£hÇêæâežò¢Ø o*tIJ­ÓZ.uKw˵V²ÊäB÷µóZSï|Ôùªßà*:›2÷Õó:WÇVÚ^¢czÉÓu^e°æQˆÊ ieŠ—ÐÊ67XR'h”jz4¢Œú3ÂNBÔBÿKô‹Ð†/ > endobj 14 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F3.1 17 0 R /F2.0 15 0 R /F1.0 8 0 R >> >> endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode >> stream x=Ž;Â0 „÷þŠé@·¥IV^L E0 †*¨Ôú@âç“©ö`Ù¾;}-ŽhÁ}”B¨Åɪ'˜~|qô&¨eÒ—ú§Mx#O#Ó`©Ay>¾ÃTYÆd"btƒdKŒƒ ï¸`vŠA\afÝ­r4¥yVÎÂ~Û¹²Ž£+ôí #ѧ,8ãJ)‰Lr¦P¤b>ØÎØ÷ð)kt•7…YæAö÷B0 ¨É®!¬_!üsö=Q endstream endobj 20 0 obj 192 endobj 18 0 obj << /Type /Page /Parent 3 0 R /Resources 21 0 R /Contents 19 0 R /MediaBox [0 0 612 792] >> endobj 21 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> /XObject << /Im1 22 0 R >> >> endobj 22 0 obj << /Length 23 0 R /Type /XObject /Subtype /Image /Width 476 /Height 893 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xì¼Õه߹ÀåÒ{/R-ØQTDE¢¢‚bAD¥‰"¶Ø1Ñh¢FcIl1¦hŠ_4öM¸4EÁÞ{CÁòý™á†ÙÝÙ3;}æ¿¿ûƒÙÙ3§<3óìÙ3gÞùñG¾H€H ¡>ýô›»ï^zß}/%´~¬ äƒÀ‡~}Ç‹/ºèáŸüä¡Ûn[–F³•$@$8ï¾ûåm·-ºðÂu6¶þn½uÙǯM\EY! È47ßüüæ›ÿk·±ròŠ«3Ýt6ŽH€D`åÊOn¼ñyÕ1v, Ÿ\[ûM‚ªËª @F ¼ôÒG¿þõs ;ÞÂÉK—þðý÷EÀf‘ @,^üþµ×>ëÐoÑ·pò²e?¾ûîšÔšU  ¬xþùw®¾úé¢ú-ºÒròòåRÎÚ‘Àö ÄHà‡~|úé·®ºêÉ¢âuYi9yÑ"ö“cÜ{,šH ;`ãÇ{ýÊ+Ÿp¯ËG–“1¤¼fÍÙ– @ä¾ûzõŠ+wQnÙ,'cHù78û"ò]ÈI€2A`ÍšïÿñW.»ìѲÊ-›@9yÉ)gâà`#H€"$ðõ×kï¿Å¥—`cK×Êɵµ¼›/ÂÉ¢H€RNàË/×Üwß‹—\òHÙ®¯§Êɾøê+NSNùQÂê“ „Oà³Ï¾¹çž¥_°ýd8yåJ)‡¿;Y @j |ôÑjÀÍSïW?±½Ÿ¼x1œÚc…'“À{ï}åযYO)7v2Ç.Âܩ̛H …Àí·¿ý_a7O¦ÕOlw2†/>ù„WúRxаÊ$@!XµêÓßü¦d7}ÍzJép2ãv†°c™% @ÊèpódZýÄ'3ngÊV—H PúÜô5ë)¥ÃɸÉ7kóE$@y#ð ïz àæÉ´ú‰NÆò{ï1QÞF¶—òK ânúšõ”²ÐɌۙߣ“-'<ðÀÍ“iõ:yÑ¢oó´[ØV Ü@·‡öÀM_³žR:™q;sw€²Á$f·•—_XÈ O¾ÕI\èd )¿ñ»Ê¹9FÙPÈÕ«×p $œ¦ŽZ+NSÔÉK—2ng>S¶’r@ ¤n[×}âNfÜΧl" dŸ@¨ÜÜÕZñ§EŒá‹¯¿fì‹ì±l! d•¸Ýyç’‹/þwÅnŒkÃRN^µŠ1â²z´²]$eàvûí‹" ¸ºK9™q;³|Ô²m$Ep ÜÆV†¥Ì±‹,µl d‘À«¯ÆÀ-b'cHùÓO·3‹‡/ÛD"€n×_ÿ\HzŒ%ÛRýd8yÅ )gèØeSH [–,ùàÚkŸE›¡êâdÆíÌÖ!ÌÖ@F €Û5×<ªcÌÜÅɌۙ‘#˜Í ¬xæ™·~þó§bfE»8Ìۙ•c™í °¸ýìgOF ÄØ‹pw2ãv¦ø8fÕI ý’À-${»;¹¶–ÁˆÒX³$BàöÏ"€Ûc!©/±Ùº;CÊk×òYP)< YeH-p{àp ÉêîNÆò›o²«œÚƒ›'T@·?ýéÅK.y$$Ý¥"Û²N^²„³”SuX³²$BŸ}öí=÷,½øâ\ÛØúÊ(ëdÆíLáÎ*“@j¤7€[H½î²NÆðÅêÕŒ}‘š#œ%´H{·ü꫾HËaÎz’@ d#€[ŒNfÜÎå¬" ¤@–¸ÅêäïÒ°·YG äX±âãë¯ÿOHËR¶:ãÉRþôSj9¹G;kFI&€n×]—Án!}h:ùå—9¤œä£žu#$øï³À-^'3ngxÖ‰’JàÙgßÎ|·x¼dÉÍÄ ¸€%üõœp‹×ÉR~ÿý5.û‚‘ 䙀Àí§?}<$Så'[Íñd8yùr)çùœcÛI 8Üp ékBßÉŒÛYüˆäZÈ+œp‹ÝɈۉŸ'y=úØn ¾új-¸Åîd _¼õãvn8,¹D9$€nøÃ²œ‡Ó ÉÆV¶úcpòÒ¥RÎáYÈ&“À:¼úÎ;—\|ñ¿C53÷ääE‹Öòè$ÈpûÝïj/¼ða 3žœŒ®2ãvæí|d{óLà­·¾¸å–ÿÑÆ¨XáÕɌۙç3”mÏpS’ŒxÁ«“ù(¨üœ•li> 0€[ÄvçÕɵµ —Ï3•­Î>¥KÀí!‡!£ëÕÉRþì3j9û§'[˜+ à½{K•X“·3Wg+›m àVÊq­¯ÀɌۙ퓔­Ëp{â‰7À-.ñº”[“·3-ÛH™$€ ÿþ÷«W^Énñ5sNÆò0ng&ÏW6*ËÀíÁW^qmœP[Š®ÌÉ/¾¸:ËÇ.ÛFÙ"ðÍ7ß=ðÀŠË.{´hÇŒ+E 2'/ZÄ`DÙ:iÙšŒ@·?ÿù%† J”uÝ+S™“͸=ˆÙ,Èp»÷^pKô0EQ9Wæd )¿ý6»Ê™8uÙˆÌ@·»îB·GŠžò\™p;yéR)gîdfƒRNàý÷À-}cÇwDÅN®­åÔ‹”ŸÀ¬~† €Û­·2€[ê… ?Wìd _|óÍ÷:¨ÙH%×^û즛^pôµø6½ü8ùµ×øØ‘TžÅ¬t60€[zÅëRs?N^¼˜CÊÙ8¹ÙŠ”XºôÃë®{Ö弿Gé%àÇɌۙ²3™ÕM?pK¯l5kîÇÉRþüsÆíLÿyΤ¸i:-íÉ|:ù•W8|‘†ó™uL-p{òÉ7®ºêÉ´«†õ×$àÓɌۙÚsO:ï¿G·×ÀMSe™IæÓÉK–p:\ÒOmÖ/uÀ-3‚­ !>Œ!å?äÍ#©;éYá„`· $–±Mü;ù¥—8¤œÐœÕJp˘Z+nŽ'3ngŠN|V5>ÿœܲpOtÅvlèßÉxÔwœ—ÀSUJa7ÚØIÀ¿“1¤Ì¸‰?ûYÁd`7Gço@œ¼l‡”“uʳ6‰%ðöÛ àæì*qq2ãv&Ö¬Xr0€•«C '3ngrN|Ö$^~ùãnx^ç|dÊɯ¿ÎGA%P¬RÌ–-c7ŽTx#”“·3擟Å'ŒÀ¢Eï]sÍÓìõ‘€WÁ9™óá&V'&àö‹_<åõLdz°åd )ñµ“Xl0€¥¼r%gÄ%@ ¬BäÀí‘GÀÍÛ¨i úÊd&:yñb>ž/r°ÀX €Ûÿýߪ+®x<“r`£b! “·3V=°ðH €Ûßþöòe—=ËiËB3L @'cHù£ÖFzb°0ˆœÀ×_¯ýóŸ_ºäÚ˜ƒ¡ÖɌ۹!X`tÀ-ýÓä4-X'×ÖòΑèÁ’"#ÀnÉQVæk¬“·ó{> *2S° ð |ðÁ׿û]í…>œy° !¬“1¤üÎ;ì*‡o –>pKˆ£òVÀ̸áÛ‚%„Kàõ×?»é¦.¼0”+8y3 Ûë•@àNfÜÎp}ÁÜÃ$Àn^ÂôÜÉK—þðí·?„yÞ0ož¸ýêWÏ~~1CðJ p'cHùõ×yC_ðÒ`Ž!@·_þò¯'Ó“@HÂpò’% |’?˜mž{ŽÜ8fž8a8¹¶–wó©æ,3€Û›W]õdHýfK~„ád _|ù%ãv+æpóã n œ¼r%‡”p³ŠÀÚµ à–¸éÑ(.u¥„ääÚZ:9(0_¾ýö»¿ÿÜ(äÔÉÉ‹ók_&áÆþ €Û_þÂn©qQê:´!U8$'3n§¥0‡Š |ñÅ·üã²K.y$¤³†Ù’@xÂsòŠœW±T¸a…>ùä›»îZrñÅ´1»Çi%ž“9¤\¡V¸YEÀí÷¿¯½è"pK«‹Âëy¦+ç𜌛¬·³"»p#oÌn‹N3]æamKÏÉR~÷Ý5ÞÎ.¦&/Àíæ›ÀãLÕÉË—sHÙ‹b˜V›À+¯|rà ϗêip= ¤—@¨N^´ˆýdmË0¡åËÀ-SÝÂôÊ3¤š‡êd )¯Yøz®aªrÌnφt"0[HPŒ!å7Þà }å\ÃÏËøÏÞ¹úê§rʰ$*°Ì¸å|ÃÏK@·§žb7ŽTä‹@ØNfÜÎ’Æá¥ 0€[¨=1fžda;™q;K‹‡Ÿ!ÀnIÖëœÌ¸EÔÃUÀ-‚óE$Ÿ@NæMÖúáŠXÜ.½ôÑäŸ/¬! „M '3nçFâp ûgþ©#“1¤üñÇ|BŸÍD\üñGp»ûî¥ à–:c°ÂaˆÆÉŒÛI +~ÈnùšÜ¶Ä2–4Næ²2RžÞyçË[oe7 ™ÜDãdÆí̳ŠÑvpËX_ŽÍ @4NÆò{ï1QÅÌná¼Ì9“"s2ãvæÍÈ à–Ic°QaˆÌɌۙ'×Ö¾wíµ àæ6föyÍüÓK 2'3ngœÌnéUkž‘9™q;3ìdpKÈéÌjd€@”N^º”‚Êš˜ÀíÑG_»òÊ'2p.° $Q:™q;³ddpû׿V]qÅãI8ŒYÈ (Œá‹¯¾ú>K^Êg[À-3§?’@;yÕ*> *Å"G·¿þõ%pKà‰Ì*e†@ÄN^¼˜NN¥“Àí¾û–_rÉ#™9òÙH&È̱‹”9ùÓOÀ3I :;CÊŸ|¸éÐ2¸ÝqÇâ‹.z8™Ý ÖŠ2I z'¯XÁዤ;™Ü2y²³Q© ½“-¢““ëä7Þøüæ›_¸ðÂè~©¥â4a%I 2Ñ;7Yã¶/¾’FÜn¼ñùÈ<D$P”@ôNÆ2ãv&JÈàöë_?WôðàJ ˆ ÄâdÆíLˆ“kkßg·ˆÏ8Gîbqò¢Eß&DJ¹­ÆóÏ¿sõÕO»ü”H z±8CÊk×rL9†/p‹þc‰$à‰@,NÆò›o²«©“aãG}Ü<LLшËÉK—rF\DNF·‡b7Îî#tˆËɌۑÀíÿxå²Ëþ»ž%’ TF .'cøâë¯û",13€[e§·"Ø ÄèdÆí ÃÈ_~¹æ¾û^d·ØÏ,V€*#£“·3X'3€[e§·"DˆÕÉß+¥ÜæÆn‰:§XðC F'cHùÓO·Ó×7É»ï~yÛm‹.¼á4ÓqIÝÏ©ÊmsB ^'3ngÅFf·œœ¡lfÞÄëäÚZÎRölå•+À½bÈ,x̸žŒüâ‹1€[ÞzMloÞÄëd )¿ÿþO^ÊgâÅ‹À-³ý¢¼9‡íu'»“—/çð…Û÷ ¸¹Àü”2F v'×Ö2Q'#dÐÓO¿uÕUOfìxcsH€Ü ÄîdÆít™ÜÜX~JÙ&»“·S9ù»ï~xè¡W¯¸âñlrl € $8™q;׬ùžÜ\ŽR~Dù!'ç9n'¸ÝÿŠK/e8MN« XG NÆðÅêÕ¹‹ÛÉnùéù°¥$ O !N~õÕ͈ûì³oî¹géÅ?¢¿›˜’H 'âäœÄíd·œœVl& TL !N®­ÍxÜNp«øå†$+ q2†”?û,›Z~óÍÏo¾ù¿ §™«ÓŠ%Š $ÇÉ/¿œµ!åU«>½ñÆç+Þ5ÜH ‡’ãä,Åí|é%pã¼& J$ÇÉK–ü€ÛŠÓþb·vlØd@rœœö¸/¼ðîÕW?à®aV$@9$('¿øbú†”À-‡g ›LáH”“Ó·6~ì±×¯¼ò‰ðös&ÈD9q;-ùCÊ à–·Ó„í%È$ÊÉR~ë­D‡¸7¸­¼ür† ªä‚rdG5 "ôHš“·sõêuÜ.»Œ6¦I€B$4'/Z´6ic à–Þ.kN©#4''*n'¸¥îxf…I íèä×^‹FÜG­¾óÎ%]ôpÚ÷/ëO$. tr¼q;ß{ï«Ûo_ÄAé:ŒY[È :9®¸àöÛßþ6Î̱͆@ $ÐÉÑÇíD·ßü†ÜB¼”œÆSƒu&X$Óɯ¼Ñ2¸ÅrÔ±P R’éäâv2€[©C‚ëI€b$L'#ngx³”ÀíškÀ#$@I$L'cHùƒÖ®ågžy몫žŒñE“ €;Ä:ùÅWîdwü”H€b'X'/Z|0¢Øi³$@$àN ±N6ãvÜSvGÁOI€H v‰u2†”ß~;à®rì´Y p'd'/[ð²; ~J$@±H²“kkžz;mV€H€Ü $Ùɾøæ›ïSvGÁOI€H v wr°q;c§Í €;„;yñâ ‡”ÝQðS ˆ@ÂlÜÎØi³$@$àN áNÆòçŸÔ²; ~J$@±H¾“_y%°á‹Øi³$@$àN ùNðQPî(ø) ÄN ùN^²$°ép±ÓfH€HÀ@òŒ!å? ææwü”H€b' '¿ôR0CʱÓfH€HÀ@*œTÜNwøtÚ´kÛ´éÒªUGþ‘@€Z·îZ[ñðE”`c)‹N¦?à @'Gy:§ÎÉ~âvF 6–²èä0ŒÄ<éä(OçÔ9¹¶¶ò;G¢KYt2ý:9ÊÓ9uN^ºôÇï+}T”`c)‹NÃHÌ“NŽòtN“͸> *J°±”E'ÓŸa “£<ÓèäŠãvF 6–²èä0ŒÄ<éä(Oç4:¹â¸Q‚¥,:™þ ƒåéœF'/]úCeq;£KYtrFbžtr”§sŒ!å7ި䆾(ÁÆRL†A€NŽòtN©“+‹Û%ØXÊ¢“Ã0󤓣ng”`c)‹N¦?à @'Gy:gÆÉ:q;£KYtrFbžtr”§sfœ¬3¤%ØXÊ¢“éÏ0ÐÉQžÎ™q2n²þ¡Ü=ÖQ‚¥,:9 #1O:9ÊÓ93NÆò{G%ØXÊ¢“éÏ0ÐÉQžÎYrò²eeâvF 6–²èä0ŒÄ<éä(Oç,9¹lÜÎ(ÁÆRL†A€NŽòtÎ’“ËÆíŒl,eÑÉa‰yÒÉQžÎYr²·ó[—XÊQ‚¥,:™þ ƒåéœ1'/]ê6¤%ØXÊ¢“Ã0󤓣<3æd÷¸Q‚¥,:™þ ƒåéœ1'cøâë¯KÆíŒl,eÑÉa‰yÒÉQžÎÙsòªUß”RŽl,eÑÉôgèä(Oçì9yñb:9Œó’yæ—L'c¢â?—¸Q‚¥,ö“óëÍ0[N'Gy:g¯Ÿ ™—ŠÛ%ØXÊ¢“Ã4S~󦓣<3éä+Š_D 6–²èäüz3Ì–ÓÉQžÎ™tr©¸Q‚¥,:9L3å7o:9ÊÓ9“N.·3J°±”E'ç×›a¶œNŽòtΤ“1¤\4ng”`c)‹NÓLùÍ›NŽòtΪ“—//r“u”`c)‹Nί7Ãl9åéœU'/ZT$Q”`c)‹NÓLùÍ›NŽòtΪ“1¤¼v­óYPQ‚¥,:9¿Þ ³åtr”§sVŒ!å7ßtv•£KYtr˜fÊoÞtr”§s†¼d‰s–r”`c)‹Nί7Ãl9åéœa'ÆíŒl,eÑÉaš)¿yÓÉQžÎv2†/q;£KYtr~½fËéä(Oçl;ùÕW7¾ˆl,eÑÉaš)¿yÓÉQžÎÙv²#ng”`c)‹Nί7Ãl9åéœu'go%ØXÊ¢“Ã4S~󦓣<³íd )úé-G 6–²èäüz3Ì–ÓÉQžÎ™wòË/oRŽl,eÑÉaš)¿yÓÉQžÎ™w²=ng”`c)‹Nί7Ãl9åéœy'ÛãvF 6–²èä0͔߼éä(OçÌ;CÊ￿ƺÒ%ØXÊ¢“óëÍ0[N'Gy:çÁÉË—¯RŽl,eÑÉaš)¿yÓÉQžÎyprmíú`DQ‚¥,8¹~ýêzõð$€ƒj„³b9¤sXhœ¬âvf~ÿΙsç¨QÓøG˜9ó–ÌŸ> i`œŒ!å·ÞZ×UNsVƒH€Jȉ“—.]7¤\ ד @BäÄÉ‹­¥“rȱ$@.râd _¬^ý½ ~D$@I '#ng€³$@$àB ?NÆ£ \8ð# Hü8¹¶ö»$gH€HÀ…@~œŒ!eüˆH€’@€NNÂ^`H€HÀ"@'óH  ä ““³/X  “y @rÐÉÉÙ¬ ÐÉ<H€H 9èääì Ö„H€èd$@$trrökB$@t2 H:99û‚5! :™Ç $‡œœ}Áš Ìc€H€’Cà®»^BËœü%;kB$@E üùÏ«r"dÆO.zp% @¢ÐɉÚ¬ @Î ÐÉ9?Ø| D “µ;X œ “s~°ù$@‰"@''jw°2$@9'@'çü`óI€E€NNÔî`eH€rN€NÎùÀæ“ $Šœ¨ÝÁÊ äœœó€Í'H:9Q»ƒ•!È9:9ç›O$(tr¢v+C$strÎ6ŸH QèäDíV†H çèäœl> @¢ÐɉÚ¬ @Î ÐÉ9?Ø| D “µ;X œ “s~°ù$@‰"@''jw°2$@9'@'çü`óI€E€NNÔî`eH€rN€NÎùÀæ“ $Šœ¨ÝÁÊ äœœó€Í'H:9Q»ƒ•!È9:9ç›O$(tr¢v+C$strÎ6ŸH QèäDíV†H çèäœl> @¢ÐɉÚ¬ @Î ÐÉ9?Ø| D “µ;X œ “s~°ù$@‰"@''jw°2$@9'@'çü`óI€E€NNÔî`eH€rN€NÎùÀæ“ $Šœ¨ÝÁÊ äœœó€Í'H:9Q»ƒ•!È9:9ç›O$(tr¢v+C$strÎ6ŸH QèäDíV†H çèäœl> @¢ÐɉÚ¬ @Î ÐÉ9?Ø| D “µ;X œ “s~°ù$@‰"@''jw°2$@9'@'çü`óI€E€NNÔî`eH€rN€NÎùÀæ“ $Šœ¨ÝÁÊ äœœó€Í'H:9Q»ƒ•!È9:9ç›O$(tr¢v+C$strÎ6ŸH QèäDíV†H çèäœl> @¢ÐɉÚ¬ @Î ÐÉ9?Ø| D “µ;X œ “s~°ù$@‰"@''jw°2$@9'@'çü`óI€E€NNÔî`eH€rN€NÎùÀæ“ $Šœ¨ÝÁÊ äœœó€Í'H:9Q»ƒ•!È9:9ç›O$(tr¢vGaeÎ?ÿÁ9sîœ:õ꣎ºðÄ9wîÝ\ð¯Âd\C$ trÒöãYgýéÀgo±Å®]ºôkÞ´•a²ñ«Ê¨jÙ¼m·n¶ÚjÄ!‡œyöÙMZX Š ÐÉ£ vÃ3θwܸ™½{o_UU74Œ†±§È$‘3E~)r—ÈÕ"óDŽÙ]¤¯aÔ—uº®_¯~ÿþ;tÐ\È<Ø*17 è ÐÉÑ3w”xÚi·l¶ÙÎV¸¹aLùƒÈ×"?–ûûHä&‘q"5f_º^U½m¶Ù Žüù–H Eèäwz¶C‡—¢g|„È"kÊy¸¨¨¿25~:Í" êWï¾û‘ >c»X4 @ÅèäŠÑùÙð¼óþ9zôI5…E1ñߊT\èçÇE™}f DcPšWýì#nK± “£Ç>þ7é¾9lÜÇ0î ÈÆÊÏ?ˆÜ*ÒÕ4s¿~ƒ.¼?ú²D Š ÐÉ£«lÙ3oiÓº„¼ Ò‘ ¥_— GOC";m:wî]•U•[‘ DO€NŽ’ù±Çþ¬qMÓ†®¬‹QƒúèJŒT·hÖfúôë£l&Ë"¨˜\1:¯b.1æ­µ4ŒG"dKìid5Õ5“&]ìµÂLO$=:9æÇs9îõèiË#²¥ågEÚæcœtÒ¯¢i,K!¨˜\1:ý gϾ£I£æm ãµÈ…li¹V¤±a´nÙ÷•èï5¦$XÐÉac?÷Ü¿wéܧžÆ$dKË¿3/ùõé³='È…½Ç™? ø!@'û¡§³ívÛí^«--Ÿbjy·Ýש6Ó ÄB€NûAÍq‡eÅxÿ]+²‹©åI“. µÕÌœH btrÅèÊnˆQ ÌCëbŸ'ÃÉøFxG!5:vèånZÄ_Y\eœsÎߢ¬¶{}PÔÇ= ?% ÐÉ@ÓÜdÔ¨Ñ-ýub„lõÒÏ3»Ê˜˜§ÙŠ ’͘q#ÚÞ·ïàÖ­;7lØØ,Pª«á-bî³Ï »¤™í±Ç^9dȸvíºWW× DjjÒ¤%FÅGšVözå´i×q”õçšéüóÿV‰8`VÑê!Š5F~ºuÛ¬qc|³­ ÊWSÓ¤}ûMÍoðàýŽ<ò'ø.º!W’€>:YŸ•§”ˆÔ´I‹Þ†ƒx‡,¥!ÒΨjצ Dä©E:‰?þç={´$ìþo8â—¼¤H¿9eS¢‹¸õÖˆ÷ìíÕ§ÏóæÝ]˜9´Ö¬YkG^V×Ô±ÔR_.:N>÷Ü ú´Ê³mÛn…·¢ï¿ÿéõë7PiÔBÑúàSôá [Ä5$ I€NÖå)Ù‚i\Ód+Ã@D G7µ‚·Ž~Iä ‘¿ŠàÀÅ"èëVÚä‘nFU«íæäÇ/z%+,TUÕC‡yÏ=9ì°s0þ0uê5øi¿ÿþ§ÁÛMš´°§ÄzÛ3ϼ¯iÓV*MMMÓ‘#;aüvÖ¬Û'L8«ÿ¡êS,ì±ÇdGÖÛ²NF†öÞoÇŽ½Î8ãެ0Ža/ •G£Ž:ê¢ÓO¿Cʈï”É“/1bRçÎ}¬”t²ƒ!ßz"@'{Â¥™xüø3pzúì$?-2[d ¢Ú¥`.#ŠâÊ/òX¥r¶ºÊG}™f‹\’ÁK]»ö·×â…²Jm•vØBôH­M

¹û öi§ÝºãŽûã«Ç‘ß’€>:YŸ•~ʇ#ÐPÅÓ-þ'2ZÉ¢Ülx¯w3¿jf‹ˆúú*•r§Ruă«4‡D0à°÷ÞÇ#½Ãɸü§Æ0<[ØqUÕÅÝ"ݺ P¼±9Â÷9¶RoÇŽÝ0[“îÔúR è¨[9#šh©4\Oe ÐÉeyJ€è‘81Ÿñ¢Ö ”#D&zÙГ~‹&Æôf¼ ï¶Ði2¦Ø*ޱ‹`"Ú{›ˆ{éR“éÓ¯WÀ—Bá¨x¡“‘†Áí÷¹àÊ`©Û·çÍ»G»ÀL?÷" b)[õ)Eqi?":ÙÄç[ø 'æ}^Ô:S™Eì¢ò iåufÑŽ»›5  ‡i«¸¸çÑÌÉ`B5|RpÕ¯è¶0d¿~CT÷¾0YQ'#¶ÅÔ µ-æ!c»ps¬±ž¥h¥D8¸¢i¬•ö”Çs¹KJ~Dîèdw>^?µfØ^ëE­“”DæyÙп¨šEW<ø©z†È±4½²*•Þò`G"l…ÛýúõÛޅ]ÊÉV¹¸áEÝ„ˆI}E¿˜0ÿY]Äl: YÞˆ;1mOeȽ3¥Èp}ÐÉÁîeëýÙ^Ôºá^8‘Ó¼lèßÉVÑ?±È6mß}§EõA(cõM…»­qÏݘ1Ó¡Mü qÛµú <¯hÑîNÆ&x–ê“7hаèž'kŸÛܪUÇáÃ'bxxŒ`ãÉ*@j‚o— ¥E+É•$à @';€ø|kÝÅpœµžaóË/úwò~"ˆ½_q“ñs^Õ½pzpÅÙbCŒñvêÔ[e^j½Óýö›Qª ²NƆ˜s‚˜ùVþÈm̘“ sƒ–Ñ]/UµùwÜU…›s x"@'{Â¥“¸qMÓ1^Ôz¹:«Eö÷²¡'ï ‚ i:*šÆ>õBÓòiÑÜ+qÍnذ ê*› ÒúEtPÝGnuœŒB_¡8Uþ(´ð.? 8ãÊ P‰Õ“ê3ž£ù|›[tr໾G-ÛUßkÛõêÄ顽•!« Ãç$tUõ·ÞzÀaâ’âdbª0æ°µlÙÁ|HôvxÈú·…æt”®édl5wîÝö[BJE«8ýôÛF> Ñäߪ b/ãIÖx²•£h¾%Š ÐÉ£+µ¡- !,4µù’š¹€ðÈšúLvY\©ÁØR­s¬GOÕ^}<}Ï‘Àý-¢}âÞ÷4ªï䊋à†$8:9p¤ð 4åi…ýivˆ¢ìS¶š›OÆ“þ £ðñ^ 0…Ò2®”N“(•ᡇž]tZE©ô^×ÓÉ^‰1}ÐÉaì…Ö­:âÉJšnD²Jj"[Dâd ­´3ŒîÝ7óß|LT¶O„À,„—/|Þ¨½ Ä=Ư~«Ñú·ç ³L'ëPbš¤ “ÃØ#;îx„³JÛ®¾hlÓ2Ièû¼²”OšÅa*W ÍG|6»–‘7f…á¹¥™6í: ÏbXcÐ/tmôRÒÉìf’tr»ròäKa›K¼¨3“Õ«™È2/Û~*r’—ôиuó ÿ§5)z üsT4Ú¶íìl U,°Ÿl§Áå´ “ÃØS¸{·]›.m s4û±_›£JeíDžÕÛö)‘>Ÿ‘ú¶H#ÃØ$ˆ§59è៣¬ZäXÀ͘Ãàò8ݺöÇS<©3!ˆž=sG‰ï;öÔŽzÀ¨xá‘Lìý"¯‹àæ;‡­·ðð*L«› ‚ÐsÖ†]»ô?~>g ;ðò- ¤‹œœý…Ér#G×­ÛÜmiñ‚:Up1Vä3Z2µ3ªÖl>ä´g7ž=ûŽä4„5!¨˜\1ºð6œ3çΘ5|ø¸)»OŸí;´ïѨáºçõ5®iÚ±COŒBï°Ã¨#ŽÂÓîÎ8ãÞðªÁœI€¢'@'Gϼ²9¥­2nÜŠÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NN×þbmI€²M€NÎöþeëH€ÒE€NNòþ:ë¬?͘qãäÉ—LœxÞ‰'þrîÜ»±3Éû‹u#ÿèdÿ Ìáä“oÀ#­{õÚºm›.Õ ª­§ØÿÅÓ®[4kÓµkÿí·ß÷¨£.:ï¼X:³"ˆû.@¦M»n·Ý‡‡-ýâyO;Š,rŠÈe"¿ù£ÈÕ"gˆ)2B¤wÝ3øðü‘­·Þ½èsÏý{Â: ø$@'ûèssè´]Û®–ŠûÆ|‘ÿ”x.ªãa©x‚êU"ÃEê™7lPƒöÂ…ø¬7'ˆ—ÿ“NúUÏž[A¨m ã\‘¥z*v˜o?ùÈ ÓÌÍ›¶:ðÀÙ|tu\û”å’€t²†^sÀ¥ºm·‰‡S70ŒÓD>­ÔÆv?ÿ r›HWsL£s§M§L¹Âk­˜žH èäˆ÷ÂqÇý ŸF¯v‘—ƒ°±ÝÌ_‹œ'ÒÄ43†2"n‹#ðO€NöÏP?‡ñãçׯW¿µQõÏ ml7óÛ"»šC[m5âÜsÿ¡_=¦$ˆÙ.ØcÉ0%¦L¼¦-9+r”©å=¶Ä$çÈÚÈ‚H€| “}ÔÙ³ˆ1€ Gî"òQøBV}fŒc"íÚt9ýôÛtêÉ4$@± “#Ø;îˆÁc9BÝW%Ìh0·¹Ú0Ú´î´`ÁŸ#h©ÿ"fÎü-æZçœó7?bÜFeuÊ)7ùÉŠÛ’@dèä°Q[=d”¦FDãaG)·˜ƒýú NÅ9k„Ǭ²Ìšu»Ÿ½sÆ÷Zùàß]v™à'+nK‘ “CE=}úõ êW÷4ŒOb²åç馛R1ƒNõ€dæÉ'@'‡·pq­M«Ž5†ñßX… -¯ÙyÝØ²qÄ„×Þ@r¦“ÁÈLÒK€Noßm¾ù0ôOoŽ[ÈVWù‘ކQÓ°Ñgü!¼&ûÏ™NöÏ9¤šÒî;á„_@È“!dKË1G0†= ¤&’-Ff’^trHû®WÏ­1ááÕ$9fFÇ7­Ìž}GH­öŸ-ìŸ!sH5:9ŒÝwôѱ)¸²fuP“óï£fW±—Ãhu yÒÉ`d&é%@'‡±ïºuíߨ0ÞMž“ñí€YyõªªN;í–0î?ÏRNF½qãfŽqM3O])[V©¹p¸özÈ!gî½÷ñ˜ 7räq‡¶0¤§ÈvòäK÷Ûoƈ“PÖ¨QÓ=ôl jÍ›wOÙÊ3An ÐÉïú#ü z£ˆ„œœî±½&˜‚›û¶ÞzŠŽ[9jjš˜=îuÿTv;FŸ>;¨8`–ªL¡“¡µŽ{©Äj¡mÛnøHmX¸Pèä3ϼ ¯W¯¾ÊÄZhР!œéþ\|¨­ÊÞ®ñöí;¨° •CçÎ}ÐR<Ø«°Ú\“strà"ÿ çÇIu²ÕUƃ¥ÜäŽeР1J/•{âÂO1÷£ªªÊÊ¡ººæì³ÿªÒ8œŒÞ¬Q÷PU¢}aøð‰j[Ç‚ÃÉøîhÙ²½}[Çr‡=\&¥h:yáÂûÜÝ‘³ËÛ1cNvT›osN€NöÀ3L×4á]È狜]úï‘ËE®yDä ï™ÛûÉXþµé<ίâ¶OzòL«V½Þ!8z´u˺< Ä^ »“á+KÈø]åvÛ^úöܨQ3U:öÚkŠ=µlwò!ãÚ·ßÄÚªiÓ– Åx¶ÛîζÝùвý Be…'C骫¬êêF]ºôÃó¹¬Q—AƒFwë¶Yýúž´¸ï¾'ÚKá2 ÐÉÁÇû3œŒWx×f#ëÖøý˽Dîò^„2ó[æðÅàÁcý´Ý.Ÿãÿ¹§¬ðŒWÕP³okwrýú ¬iÓVŽÑcÄtÚn»}TUUõ¦Oÿµ=kÙîd++lµ:¾A&MºæT¹ÁÞ…YaMY'Ÿþƒ={Tù@¼Ð~Ñ#Ä.ˆƒtrQÚy^I'»÷‡ ›€³²‚hœ¤ Îêr èãNeZO ÛF«íü´}ß}§ª zÒ;./ª [·îì0¤ÝÉH†Nø¬Y¿+ZO»$!ÃÂ4v'[%Ž{ja2¬ÒUßÝæ3~S˜Ì^\Ññä}ö9AµË¬vù`óæÝ'•–Å5y&@'»÷Û·ëÞÇ0<éÑJ¬œŒ p»üá1ÖÝꇪN|, ù "-/0sñ#T«27GßU“$zÖVøwÏ=qlåp2/èH Þ¢Ä-Ú©¬ Eêp2:çjÛÂŒ-¨¬ǯ0»“1Å¢I“–*‡£v»øX˜9×€"@'+þæÌ¹gå©IR9C¥”þÈý"Û©3ß\8¬túRù`ýÓæ¶ûìs¼ŸVo¶ÙNª.ø1®™U›6]­­Ð#3çNÇVv'7kÖÚý9)ö®)œéÈÊádÌCs$°¿?ÿ^5Ì‹¯ûGÖ²»“1¹N¡pŒfÅ5$àB€Nvãõ#jÒ¤…U*„‰íNîÔ©·ãÓ·Ç¡PX7w'wê´©Ú³  3çÐ$@'k‚ÒI6zôI81_¨Èžœ ÕV 0½§/•²¯a`^–N»\Ò +k«ˆ‡E(´è¸„ÝÉ:³õÐ?·JÅHˆ£'lwrÿþC]Za}„[WTÆŽ=Å‘ÞÝÉÍ›·±¶ÅÕ=dž|KžÐÉžp¹'Æ/wœ˜•ÝRíÕÉs”>Dpÿ†2­þÂn˜ÕТ½{‹Ê~Š1TE0߬gÏ­\þZ·î¤½Ž†âìNÖ¹¹ÅxÈáú—c:„ÝÉ:£4V”+¯Â9ÏîNVcѸ79zús¿±ì.`‚Œ “Ü¡¸¸ƒ‰ßWdH¯N>»NDøÿ¸ŠJÄ÷bÄùo>:ÛV]ÌkvnçÐYUµFüŠ¢EÛ\*}CÜý¡ò\°à/öìNÆý&öŠ.Ošt‘Ê q0iÜl¿Ù\e¢¹ ósÀQ¾Í0:9À‹‹VŒ*ýžª=¥W'd;á„Ξ•æò)fþŸ:nÜiª.…ÝK…†Tsç\FwíNÆNÕæ¥T( |#8¦:Û\ôz¢#OŒW¨†e»;Yݵ6¶lÙ¡ìî%Te{앎šðmž ÐÉî}ôÙZFâäoÌéÊê¤~¢"'O5·÷ùlhÐÃÍȈácU¦mÛ®¥x"*šª0î­.•ÌîdÄ+•L­oذ±•-&Ω•Ö‚ÝɸëÐñiá[„!R5œ8ñ|Gw'÷è±¥µ-pŠÞ»çÈ ÎWeé Ñ86çÛ  “ܹVdS³§jO機Œ)Ðê…‹}ö|ô—1¡®QÃÆ4›mp·÷ú×Ô©WÍ}c+n(v™™`w2xÍJ­Dpþºb¥ÿÕzkÁîd|k”ýöA$ •Ûܹw9rsw²ýS~/žöb•…î½cÈÅQ.ßæà·îP[U‘$5ü²È$3Z…uFcX““-oC@íÚu ¤ùS¦ ÂÇú×àÁû扮`Ýç²å–»&PkìNF ÷¡ëNv+ç‘#U™X v'#{6LH†­¬0òàÈ oíÖu\LħvHY¸¹c„GÐNÇG|›str€€5²ZÙH‚r2B _ð7·!‹ pÏú›™MwôYR‘ÿ-'w3ªzõÚ&æc ·U«õs*ŠNrÛyçñ–îð/.¥¹jw2šVm‹1ue ³ o t8³>“åTVX°ßÌ‚èsö¬ew'#M‡=­6bøÂ=02F ´·°,®É3:9À½oÝ×ö‡Š<©œ¬ÎV—…fTÏ÷+*H nàqõTóí.uÜpb°×jNóæm]ÄˆÊØóÁ&°î”)?-¬$Æl­Ç‚[Ù½ùÅád¤ÄŒ¸¢¥£Â*þýådŒ]Uð·ŸÈ¸ÅCÌ"HvyEYUú£™ÕAÍ °ù½{ooUóÁììÀ²ªxa GìNÆo|u/ÌŒÀn˜ÙÒ¬Ùú;æ¬<¡SG·\ehw2î7ìÝ{;U L]Ûl³qÙQ=îÄú—ùpøÊÁ¾ ãda7T)X@õÚ´éÒ¯ß`T^ÍN± og/ŽË¹%@'»ëqJ¶0ŒµÞm©œ\]zÛÕ"×áœ휿¶tb÷¯ƒ)¦.`­›oÿñ®Bµã"òjE¢;*`wò¬Y·yäjÐØÖîõ‹°ÜÁÏsä ÞÚŒynóçÿÑs¾07ÌܰwkU>Ö‚Ž“‘D1]ÄÞñ.,kpKÑQG¡|›Ctr°;Ýš…û°wUê8ÙÒìC¶+}]D jwý~úƒHÇuÁ.Ûv ?Ø-a@ÕÊÜ>W§[Ž˜!ê† ë²F*0×ÎÑÉ„çÑýÆG.M€„UVÖ}yÐ † Ðquxµ?~¾KVøHÓÉV&æŒ:Û‡)¬ÑÛÇÝ+^Ëâ^1~š1tr°;¿ÍqöîÝ“úN†c1C½þ彬çÌ÷Üóè`ÛŽÜpMUlúôë±F übb¿W\"Æ…ð0h SŒ&ãmÅYaC8£ÜøŽ8ê¨ Ë§Xyr²µ †&Á*5ÇS®0¸á§ÚÜ6èäÀ÷r»¶]ûyÔˆ''o¸™¹¢pÍ Mo}†OÓ¦apeýk§¶?æ OÐó™y¼›Wàäx+ÌÒSJ€N|ÇYsq½Fì¬ØÉ+_8:á²!’FËæmo¸•¡ @V <ËÑiÿÁN'‡tÀ0[:ÙÄÿ[tëUUy½åÙ““íc^/ó­[q ä“€=®…šôëÃgq‘mN'G†:çÑÉaÛo¿/¼÷¸—¬¾“²]ãC)Oy)eHOÃhÖ¤¥Ÿ¡]wb¸²¦T¼Îþæ«Ô¤_÷¬õ)œ¨Ý‘áÊÐÉaì\Ü€ÐÄ»z±¥Ž“ çÂõÁ$ —‘ ÇGW›†t Ë {œyˆ9ÀöÛ4)"úLèäè™ç³D:9¤ýnþú»¶0•“‹Æ»@´ä!ß3×!bÎÚùCÎ_‰t0ŒV-Ú}èR€0™Átÿú·õXP”YÑÉQÒÎsYtrH{7²5lÐpsÀ Õ¢o•“í6sYÆ­%7ë嬊³Â1à©s!5Ye‹9`§žz³ús‰Ì©6Iþœü}”ÒÉáíÇQ£N„T'ê™SßɰñÁ"/êe«„ü7óvì®]ú•ºw8<ÙÈ¢Â|fë7ëe£QlE ÐɡCË¿Ððç‘"‡”þçÓE0ííÏ"kä¦Tl-¬Ä¸‡QÕ´qó¢±zB%ÀÌI€< “=áòšÓ:´ß¤a<é]¤¯Vüök‘­ ÚŠ½ôÚ"¦'•*^d>sæ-5 u6ªÞŠC˘•Á¼0vK™? €t²†es@ÌÉ*£ ÏõX­–ÑC¶žú¶õÖ{”­$ $Í^Àô°† jš†§ÙkW`ÃwD¶707dÈ8†…Œf/³ðO€NöÏP3\¸oѬu=1® ¿·\+‚n9¢ørÈBsï0 $„åŽ@@àNpï#âóiz¥ºÐ1ô+‘f†Q]¿zâÄó¢lË"ðO€NöÏÐSü‹§AËÍ ã‘oí3?(²¥9^ѪE{<ÐSŘ˜H èäXö¤IcŽÌŒˆ@w¡eÜB2ÙáAÏÕ5ˆùæò £XÚËBI€4 ÐÉš O†ëncÇžŠû8 ÒÍ cÈÿ¼Ëò«s¤H}Áðqžòf8iàuf†$@a “Ã&ìž?†2vßýȶ­;›\ée³DyM¤ÔƒV?ùÆ=†šaˆÌ¾q#Ìv;ùäÜËâ§$@É'@''dÁ¨s‡vÝ-9ã_ˆë`Tm/‚GÜMftT›ÃÅVšÆšâJ˜eÇð Ù‰¬ ø'@'ûgl§œržõ ?÷}úlß¡}F › s›V{ô8pàîx¶f¸}ôeŒ&,yæFI @''a/°$@$` “y$ @rÐÉÉÙ¬ ÐÉ<H€H 9èääì Ö„H€èd$@$trrökB$@t2 H:99û‚5! :™Ç $‡œœ}Áš Ìc€H€’C€NNξ`MH€H€Næ1@$@É!@''g_°&$@$@''ö¸à‚Í›w÷´i×Nœxþ„ gwÜÏN?ýv>h/±û‹#@ÐÉ` $HxêÔkvÙeB·n›µlÞ¶ª O)òjÒ¨YÇŽ½¶ÚjÄa‡-\¸ð@Šf&$@ !@'Ǿ#ð°Ôc޹bÈq-šµ±ÜÝ0†‹!2Wä"÷ŠüMä‘sEŽ%²%žˆj&mP¿z³Ív:øàygõ§Ø ø'@'ûgè' J´jÑÞRñÖ†qžÈ½§W¿+ò+‘}D¬'ôááPÆMX°à/~*ÃmI€b'@'ǵ N8áÝ» €;B½ROÅ?$ûLäw"»˜ZoÚ¸ùر§ ãW£X. €Ot²O€l>{öïÄà„ÔÆ|‘/ 4[(^5÷ˆô2Ÿj‡_OštQã&$@± “#ÞÇsy£† ‘CE^ ÈÆÊØßŠ\*ÒÜ43†2pÑ0âÖ±8 ŸèdŸ=m~à³ëUÕkgT=´•–±ðÈžæPÆ[ìrÎ9óTC&&ˆ— tYwÛíp˜r€a¬ SÈ–œ×š34P\·®Î8ãÞhÚÈRH€ü “ý3,›Ã¹çþcàÀÝaHt_? _Ȫόq L™kݲé§Þ\¶’L@$tr{a‡FCÈÇŠ ûª„ÍÂÌ+‰­Zv8óÌû"h)‹ ðI€Nö °ìæ0 BÞ?r+çÿÅ‹ôî½=çÈ•ÝYL@± “CÝÓ¦]‡»9úf+IF¿0ÓÔò®»jc™9 €t²†¥rÀpîÑkl‹c2¾0f²«©e„È(Uۊן}ö_<ò‚aÃÙzë=úôÙ¾k×þ½zm½Å»bÍ¡‡ž=wî]ú9ÏœùÛQ£NÄØû¦›nƒ|úö´ýöûŽÆüùÔÏ)›p»zÿþ;vë6 gϨÏ{L>ñÄ_–8þ½S¦üÔú+{ÇúÉ'ß #H”=s¼µ>ÒÌŸ5ëw˜–ƒööë7OzöÜ !M0¡qôèéÇÿs{ΞP0qêÐÉáí2"ÄMvÑwŒ K|O¤³a4¬®A¬¹ š|ÒI¿‚@ªªê™¾/þaì!‡œé>rrÚi·n¶ÙÎų©_¿³V.¼¿lÍQÊ~ûÍhÒ¤E©¬ hÜAé’¾¶Ô¶Gu¡Kʃš«âD„ ì‰4hhå³Í6{Ù×.ã€CU¡Eš6m…«¼¨^öÖÐÉ!íSt“prMv2V¨×n®) k­Y¡¶†V»fò™xȱþ›ŒÀèÍ·¶òË,vê´)˜-úè£/kذq™íE Ͻ×ÉØøŽ(›D:vì©Ek‚•šNFªùõêÕG·Ü‘¡Ž“Ñû9òX%ö²5G‚óÏÐQßfŒÒí±É– ã WIbY™“ÖÜÕò9¶Óøײ¬Í³7­Ìšu»ŸVcóöí7±•¼n±yó6æoíCFŽ<£0Ÿ>; kOÖ¬Y›ÂrÑk…ÓT²¦M[n»íÞãÆÍœ<ùd…γ½Þºug •f‚5ð®cª|°€þù¾.âæaÛÚ?Å@AÑ|tœ¼ï¾'ª¬à^Tµ0+'´nBŽz¡{Nõ^{M9üðs=ögBô’í¶Û§uëN* \ˆ:ckèä0vè¤Iã$•µ¢"µ¯èr<Öeøzúô_z Š£“‹ÒÎÒJ:9ð½‰[—Î}šîq.ëÕ‡Ôi)²•Fz{†ÏÚ¶ÝB{Ûq#‰QuÊ)7UÐpŒÖöè1P íu”V°#tüÐ.t²]¤é-u'8Œ¤úÒÐ×Ì™·8* 7n®*VjõÇ7ˆJ† jŽ|ðÖÝÉÖ͘V55M\†¦Ý ýÚ¿hp1´°&…k,ø3/öbÉØ:9ðŠ¡Eœ³gëIòsSŽñ4CãÛ†—ë¥#>3ÆI0\AÃñ³Z• Ib>€~&èc žziÛvC'?.ìŸ:–áOUôС8>µ&[ 0cÁñ©ý-|®Æ¡Í®©‹“ÑSUuÀW&:Úsv,»;ƒ**+|M8¶åÛ< “ßûˆÃÙ@ŒO´%‰8öê5O{«ïpÍ«n³"ïko-c()Õ)-Ì5jVW¦ÀÏ¥R–Zïèãa’˜Ê WKme­·»´U«NŽÄx؊ʪìäÌ‹S‰ ;¨EŒšÛÇ~1z3cÆŽ:8Þº;¹mÛnVðÕ6oÞ=Žmù6Ïèä`÷>†ûŠsw/†|Ýì¸Zgè&"è9C›eÿþ¡´âý&Ak¾Ç‘GþÄSÛ1sX•Ù²e÷¹m:9ã*›ÊPÇðj\[9<¦†¸1 P¶h ¤¨rwÚé GúB'£¥¸ô¦6iÞ¼-æQ;¶*|ëâä¹sïV¹uéÒ¯p[®É3:9ؽ+>8ݮЪݺNx3žÛ‘².V‰üÉcqo™Wú ã©í¸§L¢£Ð²™ã†•áøñó˦ßrËÝTz<ÁP¥ÇøƒZß¡COµ¾Ô®Hªô˜ªáHæpòyçýÓÞ¯nÕª£æ¬'}4¢C­yÝ ŽÚòmöÐÉÁîÓw“ Ó†íÊ-»l…¤¨;M½ý ܦW¶G‚íÄÀ#YõÛŽI öi´š^rÏß Ídµ¶ÔÔe{x¢·Bcs8ýôÛÕz̬°oRtÙîp|Ñ8ÒØŒµuã•?F¿ç̹ӑ¾Ô['ã!ŒªÂ¸hˆ+˜ú•]œ-UI®O :9ØÒ®m×~†á`ٷ߈l4½L± øý_6ÿ ͜¡Íæã¶UÌ ÐÜÊ=™õ,+ÛÓO¿Í=1>µOÒ°OuC+TÝJÍ”sd®„Y84mw2&Ѝœ;tèá0qäéx«Š(¼³¯U¶^0ËÚQßfŒà=ûœb˜uZèÀ²k¦ÙNÎ^fxŠ]Kÿ»þ®]s“egÍ£Û{ïã5›–U†Bs+÷d¸7Då9mÚµî‰ñ鈓TzûXFwÕzçžûw5õ¢p¬Ãîd•-Ž8₲9Û¸8•·çìi¹M›.öR¸œ=tr€û÷^áüº±"IânõÚÏ5‡WÍÑ`+1j”µ}Ñ_›Ûã>ÍæãÖB¸9•Þ>¬Š¹.ê&îfÍZ—Í7ª| ¿_ìN†ljö^¥žü@¤ºîÌß¶D&×%Àÿ¸L ¶‚…Ḱآ½fóís‚š¾…q]¥Y8ÇÞõ-¬†¾UÓû÷êH0|øêS q8>µ¿…Û¡t•¸°ÐB'có9sîÀ5>µÆ40GΞmá²»“ÁPå6uê5…›Û× fˆJ\€Îž’Ë @'¸ñC¸¾ßûPåêÜ©-–Ï1‰<\,¾œñýg h6ß®ÄÊîË.ZÐŽ;î¯Z¼õÖ{Mƒ•¸åƒ*eaÙ³¯¦êa¸hp!+sûíáˆÝQXbQ'#“ÚO°Œ5…›«5îNƘ¶êúâž>—{*ñ€]Ñ®I“–ø"SEp!“èäw+ž²ÑѨҷbaÊ¿*^ ÜÕ"ë¯ô ¥ö…¥XkN5Ë* øPÈAÍQUÓ¹ŽV4“•YUCÁÈçv J`<øc,ªôNz¦A2(]¥Á ·Â0Ò ô¥R7½ »”“±9úÆè!«R0gÃeº²»“‘›ý’%†nŠ>K®¶'ÕVlÈ¿l “Ü¿ˆºÐŸ“Å¢sÝ9ßIoíF]7Ó®î…_³ö*XF¼e¼4û]ïPW² #Zö—»>U<ÛH匥ÇÏó3ÎøÄ‹Î$žåa\¿~eŒQbõ¹!–î^G\#¬GA˜W‰iíÚu· ¿ø2BŸ¹h¹2Kèä÷æàÁ˜ÅVæaeå9W„"÷o,Þ}ë>ªy}ãÊf[˜`,<а‰fóá"{¢c½Rsòɳ}RG]E9M­Ýg£ÁÀjΘm«j5J Vöí;¸Ôp»“­æØûÛ·ßè­Ú«ê_8µC¥q S[ÕCm««kTU­\ Ô¹­FåÌ…ô “ÜwÖ$Þ•þlù¢íl<Ä–Õ;xDHÝGˆQèX¯k‰´o×]¿ùöÈÃxªþ†:)ѧE϶®}EþÇ“Mtî¿Æsý0å¸Èöu«ÐÉÇýï.ý|'£E¸…DYyÚïa±Ú«>uq2R"$2ÂÚGTêjºáÿ^½¶AØ"ŒL“tr€;Ñzrľ…¹sÝéˆÞÒ§u¹m¸·Yä÷u+½zØž¾›Q…“]¿ùVx%«jè²â²šþ¶:)1ø€‡=áÓȼÀº_ô¸5®ö†w”`Ö>”'àî —@p ÷Êh:™à)Ø(źÝ#!ŽlÄö´>ÚçO +€H¤Ã‡OD íºBˆf _ô.Ìk2C€NpWâjÉ=¾…yƒR’B˜Y"ݲne+Äǰ۵²eü¤Ç)ï©ùxÆ}]-0ìàisLÖL?#Ì‘5²ª¹IÑd€ÅwÇgÜ«_U}'-ÑçJÔû̶ßï3+nžRtr€;A ¬«| ó õ3~'3·ç• Ep_Je¶oõ¾™!~Å{j>udïÈáÖl}ס»ˆ@—žŠ‹%q¼NŽ¥É,4QèäwZbÆïø œ¹!º„ùsC ‘ÿ‘ÿL'ãÎ^›?rä±¶ïÁÝÙ¸Påž â¶a6¶*|Ÿû†±|J'Ç‚…*t²BȦ(#ŽÄßÚ|Ô&>ÌÄhW÷—¯ìÝÝŠ—F #Ã(:'Ö:ƸhUWuÿcøÓÌ0ìiŸÉ€d¸qóDNM{È•“I÷Úãá5Ü‘òS° “í4ü/[1~ÿ„9ûÔ‰oÃ/ÄĨØÃjÃ0ãÂ0ºwß¼âöb†‰2m]5×I¾I“˜RÛ²e{LEPëÕB*bšÕOÖœwQñ.à†Y%@'»g­g^Ì Âœ?Q.«[¨ù0ˆœŸ13ôùü&Lðß^WWÇâÿcžÛ>ûï2-ؽà'7:Ù=nëŸìŸ¡#LúíãýQ#ª«Þ´MH¶L‡pÃêS? ÌìJÝ€æhŽû[:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@Ÿ¬ÏÊJI'{%Æô$@úèd}VVJ:Ù+1¦'Ð'@'ë³²RÒÉ^‰1= €>:YŸ••’NöJŒéI€ô ÐÉú¬¬”t²WbLO$ O€NÖge¥¤“½cz }t²>++%ì•Ó“ è “õYY)éd¯Ä˜žH@ŸÀ´i×NzugŸýWýR²”’NÎÒÞd[H€ÒN€NNûdýI€²D€NÎÒÞd[H€ÒN€NNûdýI€²D€NÎÒÞd[H€ÒN€NNûdýI€²D€NÎÒÞd[H E.¸à¡óÎ{xáÂG,xtþüÇæÎ}|öì'ð/–±æœsþOSÔœ ªJ'E’ù@®À¨0ç¬YOžtÒ³S¦¼pøáµ¸tß}_>|åŽ;¾¾õÖo÷ë÷þ&›|ܱãgíÛÞºõ—-[~Õ¬Ù×Mš¬nØðÛ ÖTU­ùNç¯^½µH_Só ¶mÞükäÓ¦ÍȳS§ÏzöühÀ€÷¶Ýö­¡C_1båèÑ/tÐÒ#ŽXtì±ÏŸ|ò3sæÂt?Úá¶š0WGŽ\qê©O²7éä@02ˆ—föî¹ç˘$¦i& ƒ@×®Ÿ`‚ÊYgùšÎA'Ç{*±tðCWë>x æ§y"~Mä"Lj ™ rÈ[‚º]ä(‘a"'ŠükãO_Y òŸº•/›oŸ©{kMû»Èl‘w7^iŸ;‡BwYUà"ƒÌ*Y‰?9Id€H=‘‘ÍD戬´mµH䑽ÍÜ®¯[ÿ¡ÈA"ÍDÚ˜ D5¦‰Œ®û9Ÿ,²¿í­UÖuf&ol¼~ŠÈ˜×X‰ÝþÅHFž:ê• kÐÉ~ÎnKqÀe»Ýv[…©\ñG‘Ö²þÕI¤¾¹ØX䢺|0â1Ù\ÙTd+‘j‘*‘ ë>…‹þf~úëº5ÿg¾½ºî­%+H¯Å¯´{l’™ ¶ Áæú§Íõ+Dz˜oñÕpŠÈq¦3Q™Ÿ×mõ¾H3’áûåúºõ{š+Û›ß;[‹l+²½HǺOQÝDð©½>Xžonµ|ãõ;‰tÙxc+··M›~=nÜr¯éä¸Î)–KÀ q»vŸ{wÅ­¦v`àkë:±Èä!˜g›ºÜî6ÓŒ±†AÞÙBÄy¡.º²x¡Wiéèaó­ò¤µr¡¹rQ]šBqm&øßÆ ð/ØJ€ùö7uo­•/Új‚ðºtãø"Àk׺ú¯5}Ž5ílÉv7»ÐŽZen¸Ä– Й·Ëܱ‰ÖÛ^½>:ýô'õ÷5¬ÏŠ)I v¬:ôu/#Ê«EzšƒÏl¬$@gû¶º•CDˆØÂ?fÊ꺚oY÷öóí•uo­âÎ5W:”«j‚…)fåyë#«þ3«ef‚±gkÏËV)OnœæsߨVâ{§¥ùë@mŽÞ8Ö¨·Ö‚Õ·wtÝw+Ö£vlXþ-î Ç$:Í3ûYÆ €&ŒWtîüiLÊ;ÁÜ=[¼-·9zÑè:òì!Ò·nå¿Ì|Ô`…eì+ê>µ6Äp1^Ïo¼Òž'"ðzÎ–à¿æwÁæ"kÌ•4¨RìÛªåyfš—l™à£Ÿš+Üx%¾hìÞK¤ùÆ °áBsCÇ÷zÔm Rª x[À ’:;šNÖ¡Ä4$¸í·Ž*&+‡UÞ3Ó¨.±út¸HCŒ`Í¿Í4WÕÕä ó­cãÏxÙ•«²²pa/ÕcGGC¾xýµ.[ä”x„ÈD‘ÃE3¿P&ˆÌªKs¦™=j{æ¨ ^Om¼r”yÉO%ÛÛˆVo­…óÌ ß#šnµqVŽ­¼½?~IÙ‰N.‹ˆ H !W͇0ÄŠ× ×Þ6ÓŒ/H5Õ¯ëÁ>j¦¹². ì‡×Euo-G]b®´.ÕµÖ‰f‚)æœcEºšogÚ2¹Í\ÓÛœ†1Èö/ºñ°·•§5Ú€d{è±ã…o ûÊýD°¡Z³¯yáòÛ|t¹¡CæWo±q2•I% }ú|PöX¢“Ë"bH \ø» dÓ9e­ÞÄœÃàÎf"ØÜZù¸™Ôg½}Æ|;½î­µr¶¹Ò1`Ïó$3ýL]û'2Á]Ìhfýúëºßõë/D|NÌ^@ˆÎ-¾Â]áUUª«Çtèð9B€6kvÒtìø"‚â^ELD(¡öí×uÑ{ôø ¢Ul¹å»[mõF Z¶ÜÇ0ª zsðà7G´uë=fРÇ0q-)nÏ=_éÛw]·ÙæÊÑ£_Üo¿1Íxÿý—5mºIãÆѹEԋë=üðE‘‡In+wôÑ/L™òüqÇ=ë­S§>§ù‡¸¦e¥{îY±lÙ9ù+Kƒ H ÉpcõÙ-Wv¹?œ#òq¹Ð)­/òª-ÙsÞú–VOšù\V÷x ¬Ù°Ò`î^Ô¥)¬Û 3ÁMæ|¹gEÎÙÔì»b•øK³ó¼…ˆ½7k•uf]k´Ñöü­A °ØWh6J­Á¥C¼ÐÍVk°p±¹)ÕÊwÍ5‹VkŠ/à’+¾J0-×òp[_£FëB=ãGíöp/æÉà«d̘Ë`—]ödN„Œf–¥Á$deì#rî}À Z+.–ºõw™É``X)?7gób݃u 0"×%uo_0ߦW­œÿÏ(À°ƒKA§š[!¥JówsÍpÛšÌ5ãE^3W¢ ÍDšŠ¼c¾EŬ±‹‡E>4ÿ¾0×ã² ^¸©rÆÂÁæ×ŠZ³¿™¦½yï Äûº¹ù¥æÊÖ"g™1~Æëœ³R™x^hÚtõ™g>Vö£“Ë"bH‰¡V‘%fš†¹±Ü¶¸‡¹xµ5gù¶1—³m…n-^Õ­Á^'‰ ÞMdsó-þ¹¾.AQwf&{Ж—ùPbC‘¯êV¢&盓÷¶™ã÷Ö}Š‹}Ž×±æG¿4×ÚsF¬ÄÈЪ d‹/‘‡EÕeÎÿåæ2¾’ðm¢^ýDÞ«ÛÊÚ¶Â1ƒ(ú:¬C‰iH 9ðì­¾¢'?T÷Ý1_äf³Ä§Ì 1 ~cãL0~›mtå_"7š ^¹Ú¼;{•H­¹!À¾ofx²yàŸÁ¢ íÝrËwDó¢“5A1 $Š:ÌÞ§aŒ2Mx鯾‚:ðÛÝ  yŽYŒWX…ÂÃ?1×lS·F§2?57yÌË&:Ù™¦{÷qùÏÓ‘C'{ÂÅÄ$xœ& x¹‹]A ¢âuÈ="/‹ ‰˜SÑQÛl¸JˆŽè´ÓUÜ<³µÆ¨ºÙÌ·øç/Ù&Úɸ®w衵-trи $Š!·óίêu›ßÙ£N€êÿî"7iËp¹¹ÙíôE|¦*Û\heŽ]?è1ÏGÍès+=nU´>­Ä\ óqJ±»|:tø ÷h£3|øáµóæ=ÁQA'G™E@2 à·ei<Ë¡!DwÇn¸*€qønÝ>ÙvÛ·öÞ{âZ€L€#ú¬ÏŠ)I pÿ/~˜CJø…>|ø*8 á}Ú¶ýÂûs¦’Ø—Æ—f©!fÑV[½Ø§ÕAp!L“ˆ¦¬süÐÉ:”˜†H0#zÆŒg&MúïA-5ê¥#VbÞ×6Û¼‹‰=z|„n6¢·Å¥n”‹yhíÛ޾nß¾àVG‚ƒu1PsàKÉ 5÷ùDéhŽü8ù™g¾Š)K!œÀO~t¶ñÛÚ´g§Ly>D¯.=t1â^ÂãÆ-Cäu131$ŽY˜í€é%xÔÊŠe!¤½œ'?õÔû!1d¶$@$œ8ù©§¾üþû‚‚Æ|H€H $¹qò{?þÈøÉ…t1[  äÁÉV'™Nê˜a>$@áȃ“Ÿ|r]'™Nï(bÎ$@Aȼ“U'™Nê˜a>$@áȼ“Ÿxb}'™Nï(bÎ$@Aȶ“ÑIþî»Ö [˜¯  1 ‰@¶lï$ÃÊ!1d¶$@$ ;ùé§¿²w’éä ŽæC$ ;ÙÑI¦“Ã;Š˜3 @P²êä§žrv’éä ŽæC$¬:ùñÇ7L·€­Wx™3 B “NÆHòÚµß×™xÃÿËs&\ð/üå™ÛNaȤ“‹v’¡æ°af;ÿsÎù[—.}ñä÷zõêWW7jܸyóæmÛ´éÚµkÿ>}¶8pøàÁûí¶ÛácÆœ|øáçNzõìÙwœwÞ?³Í„­#À dÏÉO?ýuÑN2ìóàu!dO¯ªª*H»ÿ¡Ã†rÀ³N:éWçŸÿ ÏjpsÈ6ì9ùñÇßÝ0Z±ñR¶we¨­ÛcÉžl\*qƒ {ü{g^E‘®áǹ^ÇQpA‘ëÂ⮨ˆ ¢r‘AP\ÀAGYDQñ2Š‹ŠVÃ.‹dÀeQ\žì”KH€€„@‘„¬ïgÎØ¶'Õ>}ºûTwóðŒ•>]ÕÕßézÏßýõ×%×Þzk¯‡~cĈå¶ö™S7*à1&ÃH®¬x’lvã$CŸ‡ ™wùå7_sÍí7Üpg›6÷´oßóöÛ{Ãômß¾×-·ùäÿFüc6‚DãŸ>QÀ3LNM-Óñ$ÓŸ,áóüúë_€Ìð* É|Åíˆe ¿5vÉn<ÃäÕ«õ<Éd²Ý’éöÿñ×´°ÜºuWÓͲ"p©Þ`ò?–UTh†[€Lß…´èUWÝ*4•±8…ñrÒ~kì˜M xƒÉFŒd2Ù¦G(üfÛ¶í!d2"xÃDûÏ?ÿჾ‚@Ž›nê~ãwãÿo¾¹"@î½÷ùGyëÙgg¿ùæ*Ͳ p@0Ù ‘L&;ð8™¸ÄÈ‘+O?½“C²“ßxã+¬m©_ÿ\­ÖÔÇqZ‹7"ÖºgÏ¡˜sŒŠúÎDçY… X®€˜lÐH“'NL3&Ár Ù iËFÔ¨T—Øl¼åÞ½_ÿÛßÎPW©ü—¿ü×yç]rÝuîºë©—_þÄøuy&°V·3FryùqðÖàÿJJ*W®Ìzûí¤Ñ£™ßÌèÏÙáe&ëk¯­9òK”È@F#Ìî!‘‘9ÿú×Ó .!A÷°–P«€!×\Óáúë;_zéMçœsáÉ'Ÿ¢u2Žc)â›o~mí(ckTÀ¸ngrrrÝáB\——W}ýõöèè¤Q£ç?ÁÎØ=ž‡k¥±¾Ck¡4¢ˆëÕkÄ!/Üe—µmÕê®îÖmHÛ¿ÿ„§žš>xðœ_ühøð%p)€ä/½´à™gfõé3¶G!W^y‹V³Zžzêé=É2Â3„ŒE#ݺ=[°¨2lØ’ÇŸp÷ÝOÔçŸß “‰pGŒ1žIBRÀÕLNK ÍHÂQͱ±Ù“&%GEÅ…$ÇN‹:v|ìŒ3ÎòÍɃ€äàÁs Ê{ë­ ûósyÙ_ž=‡Á±l°Šú4@¿;¯¾ú9Œõq–©€ \ÍdÓF²ÎÕÕ'’’~š2eõ¨Qþ‚3¦·:v|T±…ˆsæ ÌÔ.]ž4îAp…°c²qª›50ø»tç3~>°Q_2¼.pMã]ˆ÷Ý÷Þ;L\‚U|«€{™œ–V^V‚'YÈaáÁ'~]³f÷´ikü`9¿úêòÆ› ɦ>b>ˆU8ûì&°¥O9åTõ§a–1äX~è¡WÓÎ áïfë ˜dLj:ôSdë3ŸRm5X‡6œ62±£clÓK ¸—ÉÖÉB8ã`jjîôék½ g¼q7jtqm†Ž`®M›îð×vÉbŒ‡ÅÑ/¿¼)7r÷îƒao£ ²z6o~&Ë€/ÈÃá@Æ?Å}ÕUío»­÷ý÷¿4hлh*Ô1…€zõÄtØè$ÔÖê<~ d%… ]K+ƒÇ¡ÀM7u3ç!©³“<Á ¸”Éë×Ûe$kÁyãÆý3g~å©m2´œ± 2Ï#“›œ9ܶ¾ƒøE°¼Ï˜ <㌳„—ÃADq4kv=”ÄO<ÒHO {øÎ;ŸÀÌcÓ¦×a“,aE¸5‚^ ðÓF+ÚòïÎ º”ÉÎÉB>gfæÇÄüàËÀk¾Ø_OÚMš0¡;\Øm00táIø·Õîbõu±­6´Â»†ÎUð «\p©º.ÊØÜpذÏuá· |Š<ÒØ>iñn G´´ß‚Î-ó£0p#“7’…pÎÎ.˜;7Õ½‚K¢„ò§Á8´0Ÿ=sÕ;uê«ôS]hذ1< æÚÔªu÷ÝO©/¡”ñ[Öµë3¡ó_ÿzns¥.¹¤%~bཆLÃ[ŽB­¾ñ¸Wp#““’LÆ$ ÑþÁœœÂ÷ßOsœ£‹òjD(eiQðÈ#o ­V¸ž{î}k)l`Euîñ'ŸœfúZ0€Õ~i81°ý·º}¥ g»é«°¢{p“%1’…$ß»÷—ùó7ÎnYÁ­å€3YÂ,=pWš€Ò>:ÚÚ18pà4aP:þVïÞ#ðj0¥hâŽ`ºGE}k¢"«È£€ë˜,›‘,„s~~ñ‚éX¾7Sy¾ëÚ=/ô ¯b‘·áXƒµ¯ælð_«°«˜P ³ñ ê&ÑJ‹„í¶ƒN6÷gm÷²úÖZ´€??äXôAx÷9÷Ü‹*¤sç}úŒÁ½˜ë!kEJw1yÆŠÒÒ*!å¸4fî.ºèjuÇ”2nX;vð‚€ß#¥}uÙ>­º˜©nY]ÆÔ!‚ M\êv”2Â…ÑŒ&.Á*(à.&'%åÉÉÞ:{UTT¶té–qã$…sÿþÑ:ʈ.FŸ{ï} CØ õFûø!Px«Cˆ‚rD]@N9k/ð^£•`›·ZøÖSV}#J9:† ™ªh8ÿñÇ' gàl1¾ÆÜÄuYÅr\Äd×ÉBPW¬X‘9~|¢l–3€2#›pM".¼ð*$ЇÇ3n 4òÅY«víîW®¥_@ðÖ³X›÷?„…ål¡©Ù¯ßÛ«@ù~ýÆ›ãÈõ¤•§¯9&d•*à"&»Â“,ä°ð`YYÕªUÛ¢£eKL‡xŒûï±ÎÔmjª&XUwÞyM/¾ø¬àرL¯çøóe˜ÙhÓÈs7&Ѱ!H›6÷þÂI=õ¥Af%ï1~S†ÎÈU„çhM½Á·üÊ+‹„UL„_Bku6âëL4ï“–Õ8g ²Jdp “a$;æ&O²ÃƒÇ¿û.{âÄdÙà ·¦ùã\=ó̰òÅ!¥† ;æ–f« l¤ „b,¬Ã’:,Ž3¸Š¾k!ÿ} «)<3øÍÞE«V]L\o(ø6ˆ_UKÞ_ð\~y[´†—LqµbnþÑÄÝù³Š[˜œ˜(WL²®a¬ªªNHØ5yòj9 ÖäžxÏ=ƒ±}Vc†0Z¬6ÌÙ™:æLh$µ@ŒR7 Aªî: ÏVè p¬"w&HX+÷>f0Í9a´VÇãŪhFÌ¢ªõDþ+xø>­óD…ó‘+˜ìa#Yˆq$¦KIÙ=ujJT”Ô¡t0â†)$Ä\!L5Ò ÁÕ »«³áÇ€yŒ(1u:æl6µù‡ò /̇ý†÷t¬JnƇÄGÈY¤5LC­€@sAÂZBÞÎ ¸þDŸÍ¥Az aƒ§Ÿ^ߪí«0k ¼„«&µtóÛqW0ÙF²Î8¸nÝÏï¼ã‹¬¡ê¡‡X8! ƒ¡Ÿ_ÀaûÄSºwÿ?ü: ¸Î-K´Bá±ð%ýÉ'§ _+`ç›K‡õïêõ€Š\¸ ®¥Ótù±ÇÆ'|íX5iº“Þ«(?“ÓÓ+½êIÖâ°ðø† ûfÌ@b:3K ÜõܬF]@h6²ö^`Û«/¡”M »«¶«Ò¸º€‰EaýƒÈä ã_ÝŽR†5®_×à§X«ŽUäJ³J”¶|Õ¤Á.ùä4ù™œ˜èÖ˜d!ZÃ?¸eKÞìÙ^HL'bx‹Ç ”â+žxb²°ŠéƒÈYOHíí®L {¿.¼»Ê¨ ˜5VÑ?ˆT¥ZËá¯Ö¯kðS,ÛwBÝU¥lùªIƒ]òÏi’3™F²÷m;4gNª'-ç@D¶jÅ|"üÒ0ØP¶oT"ÄZ½eª¹ aa÷àîF( 4u‹SÔŽqauáA­5€ˆè°dE"U0‡«îªRnÙòa—xÐB$grBd*ÿç£]»#1'álᣑ¦0ó¨M]¨Y„²ÊD—þþ÷þêv”òYg]`nEví>´m{¯Ò¬º€U“ܶ¶\–‘™É0’KJ*ëFÏø]ÜÜ¢yóÖ×ä>J°üQaƒ¡*€5tj¦)嚸3©þùÏ(á¤Þ#¬Ú‰«u”~ª ðð(øCÕ燤€ÌLNHð~Lòï4µø¿yyGOLG8GF„ c§kâ"Ì,BAJ|áÖ´ tß¾ãBõZ'0~{5Še$c«n­Z_² âŸþÈÆdz’mDmèM#kèÚµ{¦M[óÖ[ÞÏ}$Õ¨×ÚëŠ+þÈT¯Ú“¥;æû‚¡o¹åAܶ@ÆÔiÞŒ­[} Õ|ëòÔWÊ8A*)üÖ©˜L#9tj:W#-mïŒkéÖp°i…602-ë¤*…¬¤’n×î$Å­-_š6½ÎàÎ,èàÏKHÅäøx.Üsޱ¦¯´iSÞ¬YßEËÙ®`®]Å1¡®Ý@¤BðܲeGìÙ'ÜØ:ˆÉp˜´nÝÛ¸`Ç+lõâO*Fö®åa2dÓŒTŬ¬ƒ11ëgˇp÷Pø»Ç†s-ø¨1oX{/'áµ6<yí:uê‹T/½ôIG:œÎ°®–ò0™Fr¤Ðþu³³ æÎý‘pÖe¡Gà±p™3öD¤D¨­©ÏGª"­LõˆÙ¨3Ìu[´hmÕNꎱ¬( “a$1Ü"|:F¸…Ý»|ðA㜕ñeº€­ê×oTÛ‚…Ÿªvèð’<‡ê[€£X™ï j¹sçÇÑUœðû’ðžÍšÝ ü]0·1Ši|XQ&3&9Â0µúòûöýè£ 5ÓLv9]=?Z‘Ù’ëÕkÄOõŸØ ¾…;îèSã[X ï[ÀæÝêºJ‘uZb"Ž®OŸ±;@"ýsÎùŸŽÕ:“Ç­R@&oÞ|œF²ÕP”¥½ƒK.ÜX“ÒYê=¸­P–·ƒÄDØ«û½êÃ9سÏn¢˜!4’AðZÞm6hZ˜L#Y€ÚÙÂÂÒO?Ý8“;uêwÙem„ÑÈ sƒFŒXf®}Ö2¡@™L#Ù tŒp‘˜.%e÷Ô©)QQô9ÛîÙxå•Eˆ»Ãú‘@42Ö§ •ÜàÁsM€…UL+)&ÃH.,d¸E„‰ç®Ë§¦æNŸÎÄt¶“Y ¬è¡C?UþdÁ"Åä¸8¦€s%êmzúþ™3çXgƯBœT "LÆÂ½Ã‡Ë$äìŠ;ÈÈ80{öLéì$1x-»ˆ“éIv'åíõއæÌI%œíÆÛw@ç™ #¹ €F²¼|suÏrr kÓ1ß¾sng0å«K8Ïdz’] =·t>7·hþüõÈ:f éDܤ€ÃLÞ²åø¡C4’Ý6/ôóÀâ Ò±d˜«†}emº÷ff2d/`Î÷PPplÑ¢MªLLç^^ù¡çN2™F²;aæµ^•-]º…‰éüÀ77Þ£“L¦‘ì5º¹ü~Š‹+V¬Ø:n\"-g7²Ë«}vŒÉ4’]0/w¿¬¬ê«¯¶EGÎnš #“³²~ çd/CÍ+÷VQqüÛowL˜Ìm^½J<ùïË;FòÁƒ¥^¸¼ï+€Ätññ;'MZÍu(òCÌc=t†É4’½O1Þauõ‰ä䜚Ät\‡Bφ 8ÀdÉùù4’=Ê,ßܲ†þðÃÏï¼³†–³ÇìRÙnÇ&ÇÆ2œoÈå]¿~ߌßβÑÌý±›ÉÕ4’ý*?ÞåæÍyï¾ËÄtN¼Ñ{ƒ·FîÂn&Ó“ìGTùïž³²þûßë¸S•æð}le2ŒäŽùo€òŽý«À·ß{ïGÂY;üTG[™L#Ù¿lòýïÙsäÃÓ¸BP>üH¨€}L¦‘ì{,Q€ßÈË;úñÇé€3³† ăA ØÇ丸|ŽI*@*Y¸pÈ̬¡AâŸjlb2Œä¼ÛŒaHφšE,°‰É4’ÿ,Q Ž-_¾b8øv0™F²Æäa* V ´´råʬ·ßN¢åLkÙ&ÇÆÒ“,z³µL†‘¼oäÆO¦†@bºµk÷L›ÆÄtŸ ´–É4’ .žDÂS 55wúôµtkxÒr¶É4’Ãg¬MBV`ãÆý³f!kh¬'éäÏ›²É ·yD±°HÌÌü˜f õ‚[Ã*&ÃHÞ»—žd‹›¡fÈÎ.˜;7•‚îµ±­b2=ÉfÇëQ[ÈÉ)|ÿý4Æ9»Ζ0Frnn±-¥T <öîýeþü °œ™˜Î|¶„É4’Ã4¬MœP ?¿ø“O6Öd wüÙÉð™L#Ù‰áÄkPë8|¸tñb&¦“tB0|&ÇÆ´îaaKT€ 8§Ó-]ºeÜ8f •ˆÏa293óÄÏ?Ó“ìÜ â•¨€ ””T®X‘Y“5”nó9L&ÇÅÑH¶cˆ°M*ÊʪV­ÚÈ€Hy³Ãa2Œä={h$GfìðªTÀV**Ž÷Ýo‰ég‡á“i$Û:(Ø8Aªªê„„]“'¯fz gàlšÉ5FòQžö P@bº””ÝS§¦DEÑçl£ÏÙ4““ìÀ(à%¨€œ ¬[÷3ÓÙd6›c2ŒäÝ»i$Ë9\Ø+*àœ6ì›1‰éâl”›5Çdz’{èy%*à22ÌžÍÄtø4L093óלÉn'ì#p\mÛÍ™“JËÙ´…o‚ÉññŒIvüAç©€Ûصë0ÓΡÂ9T&ÓHvÛÈ`©@„ÈÍ-š7o}Mî# ^íCEœëΕÉ4’#ü|óòTÀµ 8P¼`A:á¬ÿ3“a$ÿô=É®ì8C‚‚c‹mšÀg}@ùðÓ˜Ìp 9žhö‚ xD#GÊ–,ÙB8«zŒ3¹ÆHþÅ#oƒ P™@ÖÐÏ?ßZ“˜Îï>gãLŽ‹;$ÓwȾP*àAJK+¿ú* ‰é|ëÖ0ÈdÉ»vÑHöàà-Q9@bºo¾Ù1aB’ßÓdr|ø Í-qÎ:LNH`L²»Eöž Pµûöýè£ £GÇ#ï n&ÓHV›,S*à,Y¸pcMÖPéÓi1™ždÏ<~¼*@´(,,ýì³ÍcÇÆË“^C‹ÉÛ·Ѻ §T€ xL$¦[¶,cܸȧt2™1É{Þx;T€ T ¤¤ò‹/2k²†FÆ­!dò¶m4’ ~< Po*P^^µjÕöèh§ÓÕfrBîyóã]Q*`BÊÊêØØìI“’YÁ]›É4’M|k¬B¨€ç@bº¤¤Ÿ&O^më:” &ÓHöüsŤT L˜nÍšÝS§¦DEYïsbrVVa˜½eu*@¨€HMÍ>ÝÊÄtj&'&ö’¼S*@¨€… ¤§ïŸ9‰ébÕ›P›(«™œ™I#Ù¯ˆMQ*àG22ÄÄü`zBParBd?>?¼g*@lR`ÇŽCs椆 g…É4’mú^Ø, >W '§°&1¡”Î&Ó“ìóg†·O¨€ äæÍŸ¿¡t:‰éLÞº•žd¾^‚ P*ð›ùùÅ ¤Aó€`rb"Ìç„ P* Ž-^ŒÄtä>“i$Gà›à%© *ŠŠÊ–.Ý‚ÄtS¦¬Uöxñÿ 4‹ endstream endobj 23 0 obj 37038 endobj 25 0 obj << /Length 26 0 R /Filter /FlateDecode >> stream x=Ž1Â0 …w~Åa Ó4‰Wî8 ¦;)‚1T!wT¢áháÿ“ {x²ß³õÝðƒdnC –†ˆ=ª‘ÆÉ’CI ª].~-ߓ͇RÍB•i=ÙEY!–Ô袾GõEB‚àqÀ|·IÆ<¦S—þзáÜ¥ˆ.ÝãÚËbv„ßbí3c¡l‹š™ 4;aaj+øMü‡ÿïö‚¡Ëù’1Ìòlœ cE]`«M¯ðy-ÏŸé€=Ý endstream endobj 26 0 obj 194 endobj 24 0 obj << /Type /Page /Parent 3 0 R /Resources 27 0 R /Contents 25 0 R /MediaBox [0 0 612 792] >> endobj 27 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> /XObject << /Im2 28 0 R >> >> endobj 28 0 obj << /Length 29 0 R /Type /XObject /Subtype /Image /Width 689 /Height 1013 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xì|EûÇ/ôÞBŠ‚]yA…W±Ñ{GAé" ‚(" €téMzè½#½HO!!¡ !½îÿ‘ý³ïzeoîngËÝï>ùÀÜììÌìogæùÞTAÀ h®@Ž ü-? B{Ax_^AÈ' B5Ah(Ýa® ÜÐyóæ-Y²dчŸªU«öìÙsþüù'OžLK3d­Û%¯1ô Ø’€•OAè,·ôyH @&baª 4„Wöæ’Õý’‚ðŸ‡c…4hx’)2‚PÀVê(xâ‰'ÿ .L@= =öX½zõF޹yóæ«W¯ÚF¥©O¶ ü ò6ÁÊÊ»ñµ¼ Òô!€Î¸&ß¹ØXN¾„ãÎãF(¬°ê(°ƒ2¯>ŸXA¨èAáäþ½øä±B(À¨Íw¦ž‚jÔtšKÜ‹ŒuG0(ð«V­’Ï(°cø™½rçÎ]¼xqЦ,V®\yõjšü§Þ'^*«ÑJÈÀ®ûõòŒ˜ €N deÑ <~®B µ«yYAØkB)e( ‡v8âšpü¸zãyé‚ð_µ »< zþ©Ç›@šP@=âãã[4iòàÁõ¢äÓ6A(ͧšÓ,â_ùç)@¯P€: h†€#ãîžÿ“O>©¦6Ýù4ލ€æS3ûˆ h¯@Ù"E^¯PöÑ>iwR V{ΰmíîíN¾pð5¨£à©§žrÏôÛ½‹&MRo®u6И mçêS]hß| €i¨ñÄç-–þþs§M3úCôl&!{S€¾£äÏ и¿Š„÷ïßWíÁÞÒœÄf¡jO€ˆ €ö |ðê«ñKŠÅÒ¹xñNkº2È¥§â6^`‹ ôãb©K™C`(à‹ ¨ÛQ@{!ª&âx€Zàœ¤Ús "( ±Ÿ7jtÖbþÍÏ—¯úSOEFFjœçÉÑúÚhÈÖvóóÉ/áÎó…PÀÇP«£€zöìÙ£š˜´Y¿ÆÁįöˆ h¬Àðo¾Ùô  ª•.½üOƒÍ«GÿPãWä €ù Ž:õÀîÜ—<ƒ‚‚(*užŸ pjµ¹¨£Îs ( ½sfÏžž'ØK þûÀbi]¢DïöíÓÓi >7¡˜Nu|ƒY€ÆV`Íš5Î( m‹hÃ"Õžr´NÍ…„´m2µZø@*°}ûöÁ%JÈ‘@tO.P Ö /ÄÄÄèÿLŸéWÁŸ³À @Ç xÞQ@ éØeÇ)¸x…N1–¬³^:l(`BÎ;×!(È Èç˜ÅR% `ýš5z>Ö=A Õ¾zÕkJz¾~¤mbccÛµkGfÝ¥‘yàwß}Wµç¤®Mí×Ú6PmU{ D´T ))©¶$ *¸g±4,YrPºís8OW šþ©–oiAÓ(@‡Ž?þÅ_¤½ˆéع‰wÉMƒëÖ­Sí±/éÝbˆxð®j„ˆ €Æ ¼^¶¬Ý^Ñ3Çb]¨Ð_yEÍž=ö'¬§w§S•1vÀþ¾ÒÛIॗ^¢“ =é°Ž@ÊÌÌTM¶ýz·"<­Ú!"(àžÙÙÙ'Nœø}ôè&5kvkÞüüùóŒñT«X‘ì¾Ð¥}~~U‚‚víØÁ§:Á!Ÿ*øuž±@ó*@$ðûï¿«H"ÐÑÉýúõSSÚQĶ_{Ÿj>₌ ddd>|xô°aõªV}%  £¿ÿì\¹h7ÂmËG¥K7}ûíƒiË?'Ÿz¯¿~ÓP˜º%KŽ4HµµBNò%‡ŒQ»qšQ€Þ©€œ<ú¬gTmC¦Þ‡öPhÕ@h‹:qµ y¸ô©„ PÏ¡¿ B ”y¸7Ñc‚PááÌO õдäç!U½‡BLPÀ±©©©ýõ×O¾_¹rÕ€€®¥J-öó»bϦŸ¶XÚ•,ùÎ /¬Z±‚úEÙ½yó¿íÝnÛom± -RäãêÕïܹã(65ýW¨T»=l"zªùLˆ _" &PŸ™lúXÙq¿¾ú*m*¤êg­1¢ª>"ƒÿV 11‘V ùòËw_x¡F``Ÿ%VZ,·Øì8ÑÂ×E‹V}ì±i¿ÿnw¿â‘C†¬b‹J„„­¹r½V®KÿÿÂõoãQ»ÕÛfÕu pÐNk×® T®\™0@•yÊð@óÿT}W4:íÈߪÜþ¢vo )ù”tFáÛÏ=÷v`àÀbÅ6Z,÷]±ÝòŸùtÁ˜‚_ Þ¿ÿíÛ·å.\¸pbÞ¼òÀNÝ„ï”*5þçŸåñ¨ïîoŒÚ]Uý'CŒPÀ8 ÈI€kŸ€!ÐEêóTYÚ#H›îa$«üXˆ ˆ Ô¯Yó »`kÙÓ-–ùyòЖÅÝZ¶”æÒÖâýK–´ ¬ì“AÅŠ5­S'>>ž×Ëê¡Rí¦aÁ<²aÁ²aÁROS‡Ë ByA‡Ÿ„§ ¢vózÁˆWOô" råÊÕ©S'õŸŸ6E¦q ºç·wWÿÉ#8zôè'þþʦ٫´¾`‹Ÿß{¥K7~8ÿðÂ… -݈‡nY'OÕÇ?yò$——Ež×MÏcø/—‡C¤P@ˆ&NœH£44 nŸí07o^Æ58uêš ÝXÍåÉ©+ðQµjÇÕë"°5úy«R¥Þx晥KÛ^eô¹`±¼éï?câDõ_Ö|Tm"ŠÏÔ2Ä4V@$—_~Yu·*úè£è°ú}AÃVv¿ÒaI¼X¬w»As Õá¥â5ûöíkRª£]ö$Ø%‹e”ŸŸ'1¤Z,]Šo_¯í…¨¦À;õ®ÚbÃ05Ÿ qA- uÊ“&M"(P €º}dú)ÂÚµk/]ºT^ñ_{í5» ÷$,™2e /VŠ•W—Ûñz2ÄëË Ôyùe:ªØK­ñ½æËWíÉ'ÃÃÃU{k1ºÖk©1™¯Ú!"( ±tú€º$@±Ñç7Þ˜3gŽÝyD³fÍrz0"!Ý{UGß±ƒª="‚¢;wîlåú”?À6¹P‹…æ..ž7Oµ÷ø²ÞT@Û•àœSÕ^'"ÒT±cÇ:µÎòï n2âÔ-@½ S§NµZ²dõH÷ïß'fPˆÊÏϯqãÆVw©ü5ìáŒb‰êµtÔšâˆPU·+UŠ0U„IKÛ%z´i“––¦‚$?é˜[¨Â[D:(°uëVåW0Í,—òçÏO‘<ÿüóD´ãc¼ÿþû ‘+VlïÞ½ŒQ¹ìK=šŽ\‚Àgªµû:àNó+°iÓ¦ÏLØE Q9¦(ðæsÏEGG{ú6hBK·Mk†§O€û¡€ö DDDåU°ËÊ—òåËGÝ O<ñĈ#ܨÅÁÁÁÅ‹w”D™2´ 0ÿϽ‡ÛÛÖh®>X{ÈÿÅú` 5ž}6Úœ]r* å ¯¬ öô ÒNA\k±Bä´·¹&;7{*î‡2îÝ»W®\9êŸwd”ùÓ!DdÊË–-;xð`O&ÑDŽ:(¨ÛágÞ[œIR,? [§á®”6P@Ö¬ZÕ­D ¹m5¯›öZlR²ä×]ºxtøé> +µü Î;E,P@3¨®U¯^~æ;²ûvýé„âÒ¥K÷íÛ—NSU%«­Zµ²Ë$4'áÆª$ÁÉ­ZÒ‚Àzö,SÆ tÈ MÚ·{†‘º`e±\¦#-–?-–_ ìV¬Ëaˆîåá·B…Þ­\™} ÒNIh­U½–#m–b'/ð‚FV sçÎŒ;ˆl@»–*Uj×®]êžpJ¤K–,i‹uêÔÑZ½^ü[Ú/ñˆÖ…ô|AåK–ô)^Ü=Ëk÷.šìN{Z,3ýü†-Ú.  V@@Õ2eªU¬ØäÍ7û´k7î—_hà¯CÆ„vcPÅó Ÿ_•  m[¶¸ùcv!–Ûk ÜÔëˆ0•“'OvÔcokɇ6¤!7f 8U…Ž^µEšŸ°aç÷ª€Î€í̳õ ‰ÜmØT~RDç] P%ªZ¡Â ·Lóm‹…8^i±üž7ïW%K6 ¤_/WîçŸo_·î÷½{ÏøãšLãƒòME$ý¦Œ»Ø­tÙ°$îÜ+W®”uͱg¥¶Œ6®å¡¡€î ìÞ½Û% Í‹h)Á­[·8å¼gÏž49AŽ" YYYœ’sít>û½(NRÆe(àž çÎP´(£‘ý+W®.¥KX-(è êU­Ú³yóQß¿dÉ’ÄÆÆºTõÖ­[7º@Ƥ] –m±¬õóûo@@›÷Þ£#ÜSæÿïúQ+* ®ÍtrŠ›¡€Æ ÐN “üåvYtÓÖo¾ù&ºÎ/ŸÇ—g‰ð`À€ü’só1A Ñ@[þwÛ‡~8$9O! € Ф WË—¿ÃüSý퀀ÇÓ® n¤e{ Í)ê¥öùJ,–‰ùóW Ô­!Šm¢îøtRµFÛm hs$nG;ºóȸ 8S !!¡B… v§óÙÂùPgB£Fh¯cg{zF%¤ „ð¡p-‹´×1ý²(îq3BË 4qíIÚä Ìš6mh‘"Œ?ÀOX,-U¥CÛ‘5 bLÝi°Ktnrñâ¯?þ8Gاpÿ]QÆuX°š \s?w¸ h¯õÖªU‹÷IÆWÙA<н{wu':zêaÆI£³ÓÚŸ¶,üð„t»? ”=«Â:­ó‹ô|MôôôWË•£%{N­­ KÉ’4n¨®Joª,–æ¥K×}å•õëÖqlsfñì‰ñuËbÓB²g_b@< Ý¶‚@ÃâŽÊ”.ij!{Ôç¿VºByg¹¡¦ Œ„Óì±#$p_©ãÇÿ\¨#$X,ôÜýÄÜYƒâdf«™ËÒܹi,ã³ NŸÖ¤Ú„'UdeÚ—_-"t *>PÀl Ìœ9“¬­r·€t•BÎ;WãG¬T©e€’Vg‡sN¹?+«á‡Ã ôÓ £ ôÚÓ`þÃÕqœRE´PÀŽ´Ó×+eÊÐÈ»•uôuJ¾|¿e'"ϼêTªDg;JÔ‘ÿ]:X¹P!Z`8¬_¿›7µ=(M& BYÏÀ   |-¼æ\{öJp7PT€dï _ë´Sºb|\.Nœ8‘¶>øüóϹĎH¡€×)ðû/¿üV° #›këÿF@mXªº ´V1Ê$ S™z”,ùÆOÐ,¢ÕóÃ!í&4Ž¡ëOÞ' ºi5ñW‚ á>j¬O„pP€M:ž˜ hÚ!mNxäˆ>›éÐ"ÇܹsŸ9s†í™ ø´ÉÉɯ%3ÛbÚP¨Sƒ<$û®GÝlÙØa±Ô¨ÿÆÛ·mã‘wâÌJ.4?ÐO±ßàÙ‡$°ÓÜ‘÷J7ÞxCpä sH§ é˜ó¡C‡ê˜:’†&Rà×áÃ'çÏoÛàȧm©Rž®îw Î”‰úù9J—üÓ,–¹ùòQE·–-Ï;ç xS MgØ*óXhÍÑ AX/´*ùš²‡,@5 UB´¬Ï ˆþtÒÁ“O>yíšÎåÞ¥=RÔÐq@³*ðö3ϰÏ" - k>ý4§G]¿~ý/6J¢ã†)R¥lÙ‘C†ÄÅaª §7€h¡€ ÐÞÅt„Ðæ„UªT¡ù.DŠ P è§­Ù¯ ðÃÜêm08kêTNù=yòdÀ@«O[,Ÿùû¿õ쳋æÏ×`cN†h¡€÷)P¹re „ºuëz’¿÷½<ðLíÛ·Ã|ÈQŽÅòZ@€Ê;ÿÈòçÎzeʈH@imðó{?  I­Zûöí“…‚ @ý í@•G üýýéÌý3Š@(À¬À÷}ú¬g›ÑG–šŽ êݶ-sÜî¬^¦ œ8µ@Ú…¸ïgŸé¿û¨;{ €÷+0fÌiW@»}eïñPÀT Ô®Té34ö÷ åú|/=öX•òåÇI[¦sM‘C(à‰Ï<óŒ]<žÈ‹{¡€ö Ð0_u›±{«¡|éë‹¥ÎþÃ;“´rsƒy‹Œø¡€‡ DEE) <·CØ»woŸ’%%£¯ìV¨Ð’… 5Î!’ƒPÀ€ ÐYByóæ•:ì:€|qÈPP`Ä A«ÙF ²hbaP¤.A(à# T¬XÑ.È=>Rð˜^£À¯¼r‡ Öúù}Û½»×<8 @O ’[»n ' ã^( ±™™™o0FüI@À¥K—4Î!’ƒPÀ˜ ¼ð v1@î $0æ»C® €]h"_OåùâU:w¸æ“t 0>P @xë­·äÖß®H€²L¤À¯?þ¸Lñ@‰èÀÁŽ|`¢GCV¡àª@Ó¦Míb€ÜHÀõ r( ®õ«U»Æ6‘`Å2jð`uSGlP ˜W^½zÉ­¿]7À¼ï9÷5h£Ñ×™'Œ,P`íÚµ¾&ž @G Œ5ÊÏÏÏ. Hž@GêÁ MS§Nu.]ZPv´ˆŒŒ4Ú# ?P è¥À¬Y³è”CÉúÛu ôz;H ¸ªÀÄ_]˜;·2 HWß ÂŽ‚®*ŒðPÀ‹X³fM±bÅì’€ä $ðâ€Gó2š½ýö%¶‰t"aõ ¼ìññ8P x¢ÀþýûK”(!Y» ' ã^ ¸t)~ݺÈI“oÝz-""55Õwî¬úè b©+À‘#ÆbiY«–oI@(`ÂÃË/n—$O YÞ¦Oå3;;'$äöòå¡ãÇüå—=âßÖ­×ÏÂÃsBCSïßÏô)Aèa©:w`>íˆDÖ·¯¯I„ç…P@AÛ·o.\X²þv@qIcRR2¹º`Á©_Ý'‘€ä‘€¨@ü I»qÇvïŸ>yòÌ$ €‘ ÉE8öÈÈ/y¸w/u×®K3g5ê/ lVH ‚ÁÙ³™—.¥eûÀ`BÛ÷ßd›H@xÐ¥té3gΠ€A(ä ,XÐnç€ä‰^¹\pk©À•+ 6œŸ<ùˆ­õ·ëc D0 ÍŽŒLMKóf2x½\9«®…¯µiÖ…–oiA(à†qqinÜåö-NO>¸­-ntCœ¿&Ÿ$`l=@ƒðp!,,5!!ËŒü–èèèÌ ª=ö˜ÁŸÙƒ>®@VVΊaK—F†„h×Õéôä# Km?--ëØ±k ž±;IÀÖúÛõqŠ"п4ÍàÖ­ mM›TæÍž=5_>…nù¥›KƒjÕ´ÉRPÀ ââR¦N=J !ÔÕyþ|jFFޱ±ßâôä# »˜éªññi{öÄÌšu|ôh¥IvÀÖ“ Ä*’qùrõKxÁ§Sýúg™'ì¶XvéâOG€^©ÀéÓ7¥Gˆ­­¨ OMJâÕÕÙ¬Y³B… ÑRD»Ÿ¢E‹–/_Þ+5ÇCé¨Àµk‰›7G‰ lkÙÝöq AVTwöæ­6 ÐîCò®÷ÔÂC[¨¨GDÄ­\>aÂÿv—|ÏÝî!TÅÂÃÓy±·­ *ú\½zµ1óiG„ ½üý9¢b€ž+6}úßV-¡]$[-ú7$$ýúuZjí¹ÈˆA_22²¿¾hÑÙ±c÷[uÕ¿z‚{ß¾m²iK/W €B·€Õ¥÷‚‚ô-H @¹çÎÅÙm$•‘@l¸h 4&ÆKÆ@åšÀí5 \œŽ©|+b‡j+@›¼ÑoüÚL"ôs&<<ýœj(Öø®_Oܲ%jÚ4_œ$ÀXÝLb- Á¹$¬5á €î \¹’À{¸Ö°H -§Š‹ÃùËÚÆ£G£ÇÛÇh}9˜ XËÂÂrè4¥¤$L3Ю–!%(àªû÷_3f/ï&×àH 6Yô¯ÑNwõmš(ü²e.œ¤É»|9~¯A‚ãw¦‰Ê*² |AŒŒìÅ‹ÏjÓš ÄVKËíÜ}¡¤Ù}F cÕó2$ð;$$ýêÕt»ežP h¬ÀIZžo.$[-šˆuÖüŠ%ÀÇ‘@ÂïèèT,ÿáWÑ3pª-òÒxX3"ØdÑ:ëðð4 €:-T® ¤qûñ”î§)¹ZJ x·ÙÙ9´÷ c[¤b0ó"4ÿðî] €ªV?€ŒõË[$â6G÷©VËPPàîÝT½V{™ ¤V‹@¯_ǨB)c½$HÕÊÊZÆZ‹ x À˜1;[!Õƒy ˆm×Ù³™—.¥a £$`¬e¾ÓK`™/¦ec0Á“j†{¡€c~øac+¤z0/C±í¢s^¢¢p΋ã§xHÀX˸"Áß‘ Kb¿>½û!£¯-X}ñäéˆT+Ó¬ïWšå‘––2P¬N¸\WHÀ£q£PÚ€%9M–k%H #lŒû0¾yÙô –l‹Õ_îìÜO¥¾ÐænÏCçïð¨/îÅIÛÑ)çññØæÈµZ†ÐP@A {ÍË]tÎ fF)?«K@]àDdbç¸y²òZ‘€í×b%‡Ý˜v.›¥ük†fùÞ¼‰]F­*¾BwhÐpQ“uãš,çåH =LŽ]”^ÞÖú+øü'åµ5OiPq\J"$$#&“yœ×2„€ xœŠHYqéØâ˜4êR¢q`j²._F“¥P1½pÆH°(f_î¬< ÖßÑ%ÿŒ }Q×5®A,ÉÑdÚL ‡™*U3\ƒŽ0)ìºöÕ­‘Ç·|2õù\ٹĆË/Ûï±´'ßKhÜëö[¢#YíÃà8Ç…H )8+0½œ#£ïÔ¿ZÒ»¡ç²´¯A,)Š›‰=x€i µ — €L‡¡™oŽ-œYT¹ÉÊ›•ï‹;ƒNF&±4 Ú‡€ÃÎlV%šõÐ|€7ßW®DN¯v½óöu‡=E³»}cvVõ _¡€CÌ… .ïy&õE§-• Lúcã¯.coC4)Î?ŒÇÎlÿ_>š!ÁÐS¤jⶃúåhÌNãZãFrgÏf\¹’Ž=Cš\€0LŒ]é^ÛÕïÖ/n4#ZÞB¿enÝÂo h7pðbrU÷j“Õ]mïöÒ²¦x’–8f—‘‘ó¨ñÃÿP X+`$Øv!ªHf1«æˆñ+­ª¦iTž4&ÚÜ{û¶¯7Vè%Ц—`wÔeƺã4X@zYmj‡Z©Ð˜]xx*Î,³¶ø*` $ ÍÓ^HyÕi뤀–Y<[­V…S<·oûz¡hƒó.ïT¨,®^2쌅z*N3ˆ‹Cל¯·9x~+Líîövµ™² _ûA}…&—€@m€¶)¶­ v}h9u²Ñ.Fù²òÈ*X0³0Íì-šYœö,*žQªdFiÿŒÀí¢P}Ü˦›fÕ*â+ðYŒÇ#P[d·½rÕ“FÜk7´¹ H$Ð &\ vµî(„ßuE› Â/•ÈH!“|}Ö âÁe  Æ\[¨Ð¹t©ÏíŸøµ*žÇ $hƒË.v©â(¦£íoì^MLCWÌ.Àé³  :ÅõWh‘\ºôAB3÷š mî ´A:Ô@­ž·ÊÉÕ´©¼Sø¬ăËÐ æÏgÚ&ýãøV.Ù}…À/'Wçݰx?H  P)ý(¾…BMa¿ôíÍñž”yãÜ $Û¸}V‘`̘, ØÊØ@ý3*+í[˜?«@¬B…2‹HS¡Jdø—ʨ™X—%E½Â €š!¸wº¼2ÒI†:.Ù“š $ðY#ˆ—+`|$è×WÞ yâ®ó 'ï{@Í€ 3m îIm¢•´›(ïJ¡Yü@¹]€Ûg0>üpýO.ù½t(¼f-Œ  €Z"Ñ÷šÈ+ˆKîþ·F»QÈ { Àg \®€ñ‘€Îk£ß#.5VŽ_úÛ°-e H$Ð ŽEÆ?‘úœ£ú¢àO熟Ë1rmr5o@¹]€Ûg0>PÕfŸN ÐˆÑ¦î®¶‡ 4F*á´úàó;i-¡BÝ‘_*šY‚ŽL2ì±Èn×Y ÏA<¸\S ÁáóqeÒ—·K®ºižá–èH·› mn ´G±loŠ>W#ñ=åjEç6½ßÉøƒ»W[r»·Ï*` $ :N¤ÙÈØ¶l´]›{ …–w €z!XÎé샯nü8¾åS©/ˆ£u´~çù”—Þo?àÖ˜•—ŽkY4N Hà³F.WÀ,H@í­€¶µõ,>´fAãæÅ½ä€@}‘@^nOE¤Ðà¡™rO/v ävnŸUÀDH@ÍÑðëÓé° ÃÐm´‰qHD†)š2 À8H`Š*£b&>kñàrÌ…ÔÐÖ(Íï}AÚNÁ€&Eï¼pIÅFƒwT@ €w-s?@nàöYL‡b^~éhÃûh”Ój‚ €Ò0(í<óÊGuß°þ@ @¯ê $ðY#ˆ—+`R$Ú³ék.ž¦Ó¾>›¶ Pé’é@ @¯j $Û¸}V³#^ t@ šÅ'Àg \®€¥¹Ð& H$Ц®Ù¦$Û¸}V mãà’ÏñÈKcýx}F»»½i#—A7Ç͹¼}Ô —" €ÌHàNs£LúÎ-@Ÿ5‚xp¹@÷=ÚЕ¶u},íIGkJf”ns·çÑó÷Øã ‘`Ñ¢Óìå !YÈíÜ>«€¥¹° CÝ•’«8]I hÞ#ã1@ #ÌŸÒªL⫇ |ÖâÁå \jIhC³ûõ 8‚„W’jl¿í4! Hà´šp $Û¸}V K-L‡»}Ù}eÿR^9‘ªœH$P®#ü® |ÖâÁå Ø™ ±ÁÊv_ùj³{Ÿ+§$ ”ë¿«@¹]€Ûg062Û¢£ŠdS6úN¯Ž¾¶@!9 H PA¸^ø¬ăË0¶3o&¾ïÔâ; P0³0-Zt”"H$pT;xû ävnŸUHÀÒÔüu5Wv.§Ÿ%À˜«:JH$8ª¼ý>kñàr€,M ÊÌbîYÂ|ßÚQŠ@ ÀQíàí$Û¸}V KSÓåη,æž%ÌKɯ;JH$8ª¼ý>kñàr€,MMãûŸ²˜{ Cã ¹³òäÍÊ—?«@ÌB4y pfÑ¢™Å‹e”,‘á_*#๔ʎR €Žjo Ü.Àí³ Xš··#°‰'RŸs”"H$pT;xû |ÖâÁå Xšš7µ5îîù¼•ø£@ £ÚÁÛH · pû¬@–¦fyÌ÷Àö®>·r”"H$pT;xû |ÖâÁå ›š iOÛÚw7|¶]¸à(E Hà¨vðöÈíÜ>«€±©wu©`uKÃøö É €@… ÂõÀg \®€½is¯‡•‰wéëÓ)ÿ9™¤H$P¨ \/ ävnŸUHÀÞΜ‰HûOòk.a€¸Pf‘ÑáÊi €^‰áçrèpð]b”Ë¿¾W>kñàr€.5DÔ²¤—• =£#OVÞñ±Ë&$x :‡öü¤Žµ×’Þ¢­9ÄšBtTM¬Õön¯Ÿ®ÏÆ´_qƒøvýožqeÓ‰ÈD—" L´*Ç/ÛO™“m¯Òà“cW»TSxø¬ă˨ռPhhD¦'± €æE‚ÍÑ4‡ÖÖâ³øÐš¥ãIÝñü^ Ü.Àí³ Hhæj­Q+<Àg >'Ka„ ô„¦‚PS^„¡£ „ ‚pTrtÈšé€öÿé7€N `l…¨ý¡ž„SÉj5üâ ̈%´`¬ŒNƒ5¾ÿ)¿ú¥3@óãƒI† ÂȇK,Îþá3A„dí”2ewoùs¥”*§#R•Ûݯ €fDBn§¶ž1ÀW·FêU ÚYßLé¤ 4pÆŽ !HÆ Bª™ ÞoÏØ¶Øk~ï ½ZÆt@3"ÁªK'l«›{>›h Ãs‚.@-ìo¦&MÁÏ]8¡¬ L„L¾"š è¬U÷Úé®Q׿ëÒÚ0& $˜ ¨x¿üªTËÜv¼’Tƒ±¦ð$àki|6öÙ‚PÀc¨€o ÂuŽjš èäÁ|YùÝnmÄ dÒñgˆÓv H$0)Ðñß4]ГêIS é”§u„_ G3ã›QS?ÿçªÂ€4ް›—¦fA‚V÷ºyÒàH÷vŒë˯Uñ0f À¤H@%̵…R-sÃáÒ1âV4»· xÙߌ÷¾ TåÃ"ä„é\”5Ð ƒ´FÉvÆö–€ô²td¡Ý6AwO À¼H@Õ‡¦ëØÖ8Ÿöw¿Ô½ö ¸ߌ4]êðä‘ hÍÂZõõ5ÐÁ©, c˜½Q×toìfH$05ТžOãúÑÎŒ5‘‚ÑjbZS|6"ÝnÐÒH ¾uñÙ;ðç‘ =ܾ@UMÆìeodœ†\~騖M {Z@ ©‘@,êë/†TK|×i5¤5ëgn@UÃâÑý¤ˆT@{\USmS ÁœËÛYZ ã—íG‡²ÑOš‹X «`ÁÌÂ…3‹ÒöhE3KÏ(U2£´FàòKGØÍ´–!@/@±ÊüzuÑIµ©ÒÙÖ\ª†5ß›»JËÊå4- švÅg㊄|Ú"A[5å6lŒ·mXÜöÙu!Æiû K Àk@ªA{¢b§_ÙH{õ¿5zæå-û¢®K— å¨iW|6®5籯à jŠ› hÓBÏW ŠAý¡ç² ÕI™ ¼ ¤âmp@5£â³mЉˆ ^Wí@S 5&ï'4q»[@~cýø¶†mš€@ ^ÕHà³–\µ¯¢¬Sç9Ì‚Ô÷(·ìn»ç^Þ¡W›ã4] Hà´šp $PÇ¢øl,tåB‚öêHo$ v å½®n“€xcÛ»½8µ'ªD $ T©JnD$PÇ¢øl,£õF‚â‚@û!xü1ЪçRÜßJ½rr5#,Vh¬€@ Báz Hà±1ñíh4_œé§ã¿Ux&Bj¶]ˆ*’Y̾Z eØ…RC$ ¤ê ±H ‚9ñÙ(â À„"ýTxæBj%h£!Wû þ“òÚÊKÇ5naÜHH$¸QqT¹H ‚9ñÙ(BÍUx¦Cªþ´Šðû“i÷!§Ýt2©v.[•Fƒw$@ €w-s?@sâ³Ql5ÔPᘠÄJ}àü-:-åÅ䪴K¡Ð!È4s S\ÿ#çï:j è$ ôª˜@̉ÏF1ÛHð˜ /À¼H 5Ô °%:rbìÊonþF»¤n>o–néDH$°ªš}¨`N|6Š_TB:ÜN=Î+ù¡  ЩFE¡˜ ”„R‚à/‚$e¡œ T„'áIAxZž„—Tx^€š5¼ €¼k™£ø*˜Ÿb–JHàájB?@GM„öþ@ @ûz'¦$ðØ˜øp[ŒÕUx@½š Ût@ m½ÐÆH ‚9ñÙ(BŒ-Tx@m–T€@ KMáH ‚9ñÙ(î ú«ð€<š÷â €îÕÏï¨`N|9 }Ï<'!¬Uá “ÐˆÌ CÇ^]òÅAµ?ª™X·sÜ€1Wÿ\{ñLHDc$ÊÁ€@ ráwH ‚9ñå(FêÝQ@ ÒTx@§Ì±ó÷i„|Yù­v?¾æÍÊ×üÞ‡Îßq•r H \Gø]¨`N|9ŠH½‘ £:ê ™ðs9#¯ÍõÏ”¬¿‚ƒŽQøáúžl‰$ ê#×K@u,Š/Çò²®T°IéŽÚ™ýQ7^Iª¡Àv/½”üúž WÅ©ì$ ”ë¿«@u,Š/DzF?$xM²Õ‘H`·‘¡cª'Õ±kôz¾šô¦{³ €@ Ýú¨'@‹âã±ÔÕ‰ ö«¦;ÀnkÓãöP§¦_!À§qýìF«ì $ ”ë¿«@ÕŒŠ/G.y4§‚Vj*$°mdæ^Þ‘+;—‚Åg¹49vµmÌÊ>@ @¹Žð» $PÓ®ør\CµE:ûàŠšr l™ iO³}å04)‘Ö-ÚF®à$ *×K@5íŠ/Ç•#m´¢‚‚p@e­VíÌÒ˜CʶžýêŒË›­"Wþ $ ”ë¿«@•M‹/G—.µùS›¸J}•VL÷;CؾrÈvw{[E®üH$(×~Wê[_Ž‘¶8®Ê“ ˆ¦sÑH`ÕÈÔ‹o£lèÙ¯ÖyÐÀ*rå¯@ @¹Žð» $àb`|9ÒTAøœ Â.^Ê ¬™j‰ï2}š‚˜;+m]˜?«@ÌB…2‹É,V4³x±Œ’%2üKe¼“ø±UäÊ_@ ráwHÀËÆøx¼³FüÅ3Tù÷]A¸ÎQS U#óQB F$pìŸXE®üH$(×~WÍŒG&MÁÏc0(+“!‹¯š@«F†3rjë´¹ÛÓ*rå¯@ @¹Žð» $àkiûIAhà.ÂXAHÑBD U#3ëòVF‹ï4Øï±+¬"Wþ $ ”ë¿«@-ì Ò:3±¦ Ðü@§ãDa‰ $i'Àª‘¡­ŒÏ9RFšWp:"Õ*rå¯@ @¹Žð» $ÐÎê %R î¡­!½¡éCHxE>„Oa L„c‚@[hþØ62¿\›§lîY®º9Î6fe H \Gø]hn| Ømdßÿ”Åî; S7¡©Ýh•=@ ráwH`Dû„K;wž×ªÕìFæ6h0þZ·þ󇶮^}>44›k±ô‘ÈF´OÈ“æ ŒÓâ €"üðÖ¯¾Zþùçó[·žÝ¸ñ‘ýۢšC7Ÿ É4Na6]N€šLHH˜0a®]»îܹ£sVŒ”<À8mÀg‘`øðm_ܥ˂6mf7iℱAóæ Þ¸tiØÙ³Æ)ÕfÉ ÀHvI£¼Ô®];þüE‹-^¼xõêÕ¿úê«E‹…„„dddh”ã%$0N“$ð$øé§¬ìÚua»vsš6íÈÊ»ç߬قAƒ6,Ztöôé4ãoƒçH`<ëÄ=GÿýwáÂ…->~~~„%K–,P ÀO<Ѹqãß~ûmûöí7oÞäžÃ$$0NK$ðb$1bçÀ«»wÿ³C‡9Íš©ÌŽÈ¡I“ùýû¯#60N!7lN€†1Jšf¤V­ZD Àúÿ¼yó!'+Vìõ×_ïÝ»÷‚ NŸ>žž®i.5L H`œ6 HàMH0räîo¿]۳碎ç4o>»aÃæêõ×¼ùŸÓ¦:|ØM5SA¸æH ¡Ù1PRVÖL`ó½H‘"b7Âã?^¿~ýQ£FmÙ²åÚµkz$ϲ$àÚθ9ÀÔH@ 0xðúÞ½—|öÙÜ–-‰þi€^`7Ý&MNœxüàAÚP kyõxfIL|·rG üË#Ož<%J” nB…*UªtïÞ}îܹ'NœHMM5©"@y³ ¯H`:$øþû_~¹´S§-´k‹æÙ¨Ñ‚±cîßl Öz IM˜çÙvµ£à_L`ó…&'$ЬÅòåËO:Õóìi@_ §$0> ¶¹oßeâò@i‹£™{—òӨѼQ£þõWœ³@cÓc¨äÞyç…6fŸÉƒº¶nÝj¨ÇdÉ @n”õu ˆ´<°_¿_|± m[÷—ºd£u »E€{FÙ wµn½xÖ¬cÇ=¬Ü®;HйsçW_}•ö¾+T¨mKõÒÖ7ÞxÃ\–Å\¹½zõê¸qã*UªD‹=áê"˜5k–¹žÝ*·@ ZÆ$€„ƒ­éÑ㟄.4âÉA±¶æÊF‹‹§M;}äHBDÄ¿b¬Ó $øüóÏ鸀‚ Š`ûÓ“.Ñ·UÓ¯ž+ ’ý®÷¤WFñ$&&zž1chÐÚ0&$ÈÊÊÞ¶íì7ß,oÚt¦¹Lr˨@³fNštüÐ!ÃÅÈ $øâ‹/ä@¶^²&Ž4]­aÆ:ÚïKZuß]®\¹Ú´icv¹€ŒöZƒ`@ymн;yòvÚI AƒÙŒæÁL¤@“& Æ3ÐQŒ*"'` 4p-¯p»§'^­=<|ø°{y3Î]@ l=c@»õ‚º¶l93`À2t˜Èâ³gõáQŒ‡t?ŠÑm$ K—.¯½ö §!š  ·4®Ý½{w»õžŒ  Œ?^ÅÑGoóñÇgÌ’‘ƒ íµÁ€NkŠØuб#m;€®ƒyìf×,!GŒØ¯×QŒî!A||<ý<¤AdG–Ââ B§õl¸víÚï¿ÿþâ‹/’†ôŽŒöZƒ`@–«ztßÖ»šÃ‡G1ž;~<™½ÚzˆT,›6mJ[Ú2Z%GÁh=£×Ì]W±ªÒ)žó1u ОE´_Áõë׳G{PçÎÛÑû¢‡•+W2FeŠ`@öFƒwH ¾U†º6m:… ®Ú_#‡oÝzéìÙÿÅèô¸%Ï‘àÈ‘#ª˜­ÈÈH}+‚ÑR¿|ù2­Çtd”úÓX?½—§žzê—_~‰‰‰qõé¨Ç¦xñâŽR¡>„ŒŒ Wã4rx oCÏ?À85åa×ÁVÌ:0²¹w)otãÐQŒ±çH@¥÷ùçŸwd;ýÉ~Õ®]{öìÙ4aœê cNh²ßÓO?­ð;Ý‘°ÔßB?áË—/Osÿ<¤¬   »©P½zõÒQI ØM6ï@%ÜÃ8Ñuà’å5~à‡G1ž°=ŠQ$ Yjd†ìš—<éW-õrWªTiĈ¡¡¡–aóÞN+ûÞÿ}W'iÐ+(S¦ÌÀÕ’îÛo¿¥= lß ½¦sd¼ë$àmèÙã¼nÅÄÜ™0]^²TŽb?þG1ª‚iiiª d}ȑݡ_©½{÷Þ»w¯¯íTðõ×_ÓãKj8uÐb Z °gÏu[²ûv_+-‡T7!#Ä$`7Ù¼C ŒP#Xò v|ý5¶I474l8¯S§àaÃv:tK­m `÷¥Ss¦€zÎiØšìcóæÍW­Z%_;ÏRbÍfáÂ….ñuã;?žÇÃ>óÌ3V/ˆòF‹ x¤¥oœ@Þ†ž=~ ¾uÁ½ÔÅ®ƒ°×Ññ@€ï¿ß9cÆß[¶D;w'%%Ó½—®pÍ…sµ£ÛÊÖ8ýJl@ÔQ«V­™3gÞ¸qC!3æ½äê\MšƒAÙ׸ªÌ¯¿þJë密¾z%˜ ØM6ï@W멡£ëÀP h›‚ï¿ß!@x8°[?øà§[ÞÉ‹ÛnZRGl@S~þùç°°0»™1£gll¬K»AÒ>U«VMHHà÷°´…2©-½)ZmÚ¾}{~Éé3€·¡gH cEP7iê:øý÷­è:ÐôÛ’³k×.—z¼%C㶃À€~´Òœ:Úßoß¾}¦žr@‡?>ûì³ìK ÈRôÑG4‹ÃöE¨ëS­Z5éÑÔ‚cÇŽ©¿Ab°›lÞ!©*f###‹ö:è×o)gT† Ù>}ú?CÔœ¬þ€'Å€-’̇–²¤d­HZ´h±zõjÓõlÓ²ïì[CÓ“vêÔ‰Î>ðäe1ÞK3¤ +V¬Èx—é‚ xzöø¦«>.eøâÅÛè:p >ýôÿ`󿍰0Àm1˜:uªK<ˆ ¨÷àwÞ™5kÖÍ›7m3i@ŸAƒ±ëF!µ}ºÆ9§Y"ô èÜd“Ö,9 oCÏ?@³boœ„¨ë`üø-4ë A=œ±cÇ4 @hèí¤$/Ù¶[·nyóæU é™lÑRGDDÕ~á… ä“Ù¤: Èœ•-[¶OŸ>4å@›þvƺFÛÛ]ûo÷‘)¤.‡GÓòOš©W¯ãC™1€Ýdó $0c R+Ïb×ÁW_yù¬ƒGplÓ¦óÞ¶Å€6Ñe·ÝñæêÕ«4ôðÆoPÏ?»¡´k=íz’]£ßãô7nœmÎuñÙ°aƒ4Ro7Ï’'mFtàÀ]2I“I´;wê’º6‰ xzöøÚ”yã§â5]ƒS€÷€ÝBU³fMÉ9r¤±~»·“ç½{÷hÇžºuëPHÕ§´Ì›7ÏQêZú7hÐÀ‘D’?-ý Ðw¹%­÷¤9Z*£qZ@v“Í;$@ãÂoüä¨ë`ýúfé:`ãF/ï`,9ôË×éo|2Êìܹs•'çØ½ž<ð6ôìñ x”pŒS¹ë C‡å4@=6D4(§N²k ÉÒñ;¹oݺudg]]¡@ëhåczº¦BÕ«WwD)¢?@ƒR*%$`7Ù¼C ¤b ‡* ˆ]ýû/—àÁM[{UžÂ "yå•Wl­s}´3gÎÐX€«óÉþÖ¯__³yõ·oß¶ËKr¹€\ˉUä@Þ†ž=~ UáÄW(à ,[¶ÌjF=Mç£c‰x?Ý­[·ˆFœÚ\¹ý%7…§ ¼ó&Æ?eÊ…¹bÆ€Ú¼ 1 »ÉæH eÉGZP@3h·^ÚsOny‰ÈSƒ І{t*¢ò`½@ ^„”€·¡gH K8 €—)@gö‘iuæùå—Z> ­Gp£¯€ŽæIÚÌÙ l¿ x¿yü@v“Í;$@^2á†Þ¤­ï“€ºÊµß•—Î ruÂ!åsÓ¦M\ßB:ulÀÊHÀõXE$àmèÙãXN|…Þ¤@£Fh»B²wtN±.Ïuúôi:#À¥ ‡Ô·päÈ~¹mÓ¦Ø~ðÓß6f »ÉæH`[>á¼FƒÒ9zôS}æÌ™z=ÔÍ›7_~ùe§3úäv™fBFGGsÊpÿþýåiÙu 8‰o7Z oCÏ?Àn…'ðh/ ú‘®ýv@riÂa³fÍØ'Ò>Kݺu“Ç ¢›Ng¶»“œ €* î4* »ÉæHà´¸"0µ´S_ëÖ­ð´ €}Â!unpZ±`Á§ÙhY`€¼ ={ü@-K>Ò‚Ú+ššzâÄ íÓµ›"mŒàÔ‹¿ÖiÉ$§]6nÜHò>[7Àîëãä $`7Ù¼C 8rD  €]h³åÒ¥K³L8¤Ã íÆà¡'Í]´Ú±HࡤÞ$àmèÙãxX˜q;€®*@is§éøÚ|ØÕȆ¿páz œª¤e »Éæ’C…Ó²(!-(L© g4nÜXyÂ! 1üþûïª?Þýû÷ÒT—]!B oCÏ?@¡ â€ü „âÅ‹ÛvÚË}ž~úiÕ3““C‡2ËS±u T—]!B »ÉæH PPq @~ ° u#ÐfGªç–3Øb€ÜH ºæ  xzöø — à§ ÐÏù^½z©ž‡òåËËÀÖ $P]s…ì&›wH BAÅ%(ø)À‚d¬i*`ff¦ºÙpzò@]Á•cð6ôìñ ”Ë*®B(ÀIv$X·nºy¨]»¶mÏ€ÜH ®àʱ ØM6ï@岊«P pR€ ÈR÷èÑCÝ<ÐvŽr°u Ô\96 oCÏ?@¹¬â*€œ`D??¿aÆ©›:ùˆŒ>­w°û¡¡ Z¥¨nŠˆMA »ÉæH PPq @~ 0"Ùî)S¦¨› :fq‹âgçÎꦈØð6ôìñ  *.A(ÀOF$ ßìË—/ç— Ä¬»@v“Í;$@÷ê€ @ßT€ è<‚Ý»wû¦D>òÔ@Þ†ž=~ T:<&0šŒH@½!!!FË<ò£¢@v“Í;$@Å‚¨ `W€ h¦“Ä-BšN oCÏ?ÀtÕ†Þ¡#äÊ•+++Ë;OaW »ÉæH`·ˆÂ @Þ 0"‡È;'ˆ__€¼ ={ü@}ëR‡>«#”+WÎg%ò‘°›lÞ!>Réð˜PÀh 0"G`´œ#?ê*$àmèÙã¨[¶€Œ 0"Áûï¿Ï!‚™T »ÉæH`ÒJ„lC³+Àˆ:t0û“"ÿÊ xzöøÊeW¡ठЃæ”Dk€ì&›wH A*²|M$ &NœèkÊøÚó xzöø¾Vûð¼PÀ ° AÑ¢E—,Yb #œ°›lÞ!œ 9¢…P@YBڬآø¡;vìPŽWÍ®€·¡gH`öÚ„üC“*pöìYÚ†ˆ†> 44Ô¤ˆl3*$`7Ù¼C  -‚A(`R22²¿¾hÑ™={nÆÆ¦™ô)¼8Û@Þ†ž=~ W4<ðe’’2öï¿2gÎÉ1cöýòËúÛºõ:µ!!™ÑÑ©86Á8eHÀn²y‡§^ 'P x®@\\Êöí§Oÿ{Ô¨¿Dþ‘@lTò#"ÒÒÒ²=O1x¨€·¡gHàaaÆíP A+WÖ¯œ<ùˆ¶9<ƒœ°°Ô„œ´¨ç;°›lÞ!zÖ¤  € ääqÁÁaãÇ´[[$Ø´Û·3<È nu_ Tuw Ü/Ǹ @= é‚'OÞX´èìo¿í·µû > H 6ÅgϦcþ¡ö¯H ; Hh_þ“bvvvttô¶mÛöïßþüùôôtãä 9V $'g8peîÜÿMT°þv/9E±a ÍŠŽNÃüC+ýù}HYw€_9W%æ?þøã]ÇŸÚµkôÑG-[¶ìÛ·ïÂ… /_¾Ì˜hHHHݺui=¸|Ÿ˜’%KvéÒ%""BŒ¤[·nbÊ]»vµmƒ ÄNw¡?wîœô›7o¶mÚ´iR€„„Û’-fï½÷¤À‹/–.Áᕠܽ›ºs§ýé‚ví¾‚'#ˆÍòÃù‡©©©˜ȽX t')@îÅݳ  ·ÚÊn:#¦N:ûöíSNóûï¿Ï“'£¨hs˜Y³fQ ÕªUüöÚkv#,Uª”€ØÀnÉóرcRrsæÌ‘ü%Gÿþý¥qqq’¿•ãøñãþþþRȯ¾úÊ*¾z±±6l8¯<]PÁúÛ½ä<Ì?ä^¦€’EÖÝ$à^Ü=KÀ%$m%ÁÀ%»zõjɤ’ƒØà¹çž£ƒ§Ÿ~Zî¿hÑ"£!Á¡C‡Š/.eò»ï¾sôŒð7©âtÁ•+Ã'L`š.h×î+xºRMóoÝÂüC.% H 3Ý@.E\½HåH@?Æø÷gذa=zô¨_¿¾•A'»I]¶¹ˆ ’¬j»ví®^½*»ÿþÌ™3©—€,XP i„^‚={ö)RDÊùˆ#¤lÃav23³Oº¹xñÙ±c]›.¨`ýí^ò ĶúìÙŒ+W0åFå$Ф T.ÜjG'G±?ßn 999Ë—/—ƒAîܹOœ8axãÆ’Umܸ±ÕUñ+m)ÿÌ3ÏHÁÈ¡;lݺ•EÊÒØ±cíæžæR %%óàÁ+óæ¹?]ЮÝWðô Ä–“æ^¸@ósÌ%¸as $,²î a«‰˜1F$GEE.\X2Ÿ~ú©ÕÓ5JºJ3 ­®J_8 #‡¾H°nݺüùó‹ù¡1‘©S§Jù„ÃŒ Ü»÷ÏtÁ3ìì.¨`ÍU¹¤ˆí6æªUü€º“€” Z¥šS<.!åá—_~‘¬y`` U®Ä«ùòåËÌÌ´º*ÿJ}R<:"ÁŠ+òæÍ+æ$W®\v§&ʳ ·a¸zõÁÆç§LQÚ]P»¯‰ºH ¶¢áá9¡¡©ññØÿÐý¢$,²î ûåX“;]E‚]»vI¦œ´†JžMšx ^¥ŸÛ4¯@~Éʽwï^)½€–UÒð‡˜ š‰õ†VïÈ_££ïò›.¨`ýí^âRŽù‡nH TŠtw Ü.ÆÚÜè*\»vM2åä°ZÐ7nÜ8éêîÝ»áÎ;RH]`ÆŒÄ-b¨£`ÕªU ¹Å%Ã*ðóÏìZg]<¹"ؘ‡„ü3ÿ–Nàî@w2$`/·º„t "##%S^´hQ«<Ó…ÒÕ—^zéîÝ»Vä_iç"1°öH0aÂ)Ÿ´ŸÍŠ”g n)ðÓO¾…À +**-3dÀTT’EÖÝ$`*²úr h2ždL«V­j›ñV­ZIhG‚Ñ£G'Ø#Ÿ*Uªè‚òG¦‘;vì°›=xšBßD±a Ë9w.5%û:)ª@ÝI@ÊÀIaÕû²Ü>*,B”²Ù±cGÉâÿøã’¿ä¸}ûöË/¿,…´Ä¯\¹rÔoðöÛoK{S'ƒ.H åVO8݉Qz.8Œ©€/#ØÌ>œ˜vÿ¾Òl^c¾;Ír¥ üüó®AƒÖ|÷ÝšM›bNœH‘l¢;€š{÷r V®\)ÙÓ2eÊ$%%ÙMôÞ½{tlMà—+;ìö6PÌ҆Ɵ|ò‰Ý„$O—64.V¬˜˜šK@“¤Hà0£@ÉÄÐüÛ7±ÿ¡R¬  ¶¹_¿]º,h×nN³f³6œß Á<é¯uë?{ö 6lËäÉW¬8·wïZd*½;ßq ìP#y± AJJ Ù\:üHšGãïëׯW~:#i̘1´óáO•*UcnÒ¤‰ÝØ$$ Ú>ÑnÑSÞ}AnÛr$ U´Þ¡zõêÒsQ{lï‚ñX!ø5$óÿWxUA‚~ØÒ¯_p×® Û·ÿ§€Ñ:zõZI S¦òÊÎ ÁÿÊ¥!]r$L¤#G5¦OŸN6ÝóGÉÈÈ íŒÄ„¾þúk»JH@Áhd»aDω'Jy¶»øÑ è.b€FIw½ùæ›Ä Ià’1ØEÑSœ˜œìë`n u|÷ݺ޽ÅN€YÏqÕ¸«^ìLøæ›ÿïLؼù²©;€ÆlH¥\¹„O=õÔ7ß|+Ýî¶ãàÁƒ’9¦ÓìÆ#G€°Fôlذ¡ÛÍ›7mCÚ"…¡ñ‚^½zI7>ûì³.\°½>FVH €â%Ì?dA‚~Øúõ×ÿttè0§ysêpy @-`‰Çª3aß¾;ô––#¹9¥¼É‘ ŠýûóÇ 2„4”V¥‰…Guô\YYYV[Ú†¤y‰Ò¨MKp´} (]:vÙ6*ò “&9м»aì"’&--ÍnädßɔӜ=GÇÑLÅZµjIQuîÜÙn<ä)!a…§% ƒ ¢9 òðt£ü|ÆŸ~úI~Ur+ …Yºt©4ŠAóhïéF8 ®€ "# þ2ye`øðmýû¯ìÖ:æR'@Æÿ[ ÀbpMFìL Y43áðáûçÎé¿ÆHÀ«”«/#ˆ©ÉßHH ™{ê ¨W¯^Ïž=é—8—Ô¥KÚ—@ºJB …| hÔ@Ú©téÒuëÖ%ß·oß:uêÈ×2P÷…£yÊH@O÷×_•(QBÌ|àèéTRѨ¦Hà´0:uiúôÝC†¬üâ‹ù-ZÌjØpfýú>¶$Ó¢Åâ!CvÏž²yóµ£G„†f²—UB œW}¸„´ñ¯dЛ7on7çr$Ûu<öØc§OŸ¶‰è)!Aƒ ’““‰ìÆ#zRO‚Â>„N‘€R¤ˆ *HI 8Py™ƒBÎqI3€ì µÏöؖƤ¤´cÇ.,X°øðµÝ»/lÝzV£FÄ ¦>°eŸîÝ׎wtÅŠè½{ïž:•ʵ3H`[ åãÐÄBÉb:Ú_ˆæìmÚ´‰z*V¬(¶rPç<Íë³;P.Ž ÈŸb>|¸´Ñpš%úݽbÅ Ú!æ!tèСwïÞ4Ö¿uëÖÄÄD§÷R€+W®\zø¹uë–žº hèèС4²{÷î?ÿüó¡C‡œî5Ĉ” Íÿ¥ä¤¤á0 @ ïbsgãÆS&líßyÇŽóš5#N˜ÅòëÛËÂtホ:‚ƒ=êLð.®ÆïÐÂT«VÍäµ¼ ´ÌÒò\ ÀóRäF qq‰»w‡Ï˜áÓSZ¶\üý÷»]êL¸QØ´¼… hƒbšÚ'!£ý…´Ì<{Z@v­ÌH$0N‰MII—OQhÕʧ(È;NŸ¶ž™$0Nqµ›$ Ã ûí7šÏ/ñ@žvž6¢q@ÏËïdSV=ÚEa–wQhÚta×®«‡ Ù>aÂá%KÎîÚu14ôv\\JúxÎñ³#möûå—_šô çr¤[ôÞŠ§"RzÞ–?«€ È/ùgŽº6_y@H [1õ,aiŠÂO?­£]MQÐâ†æÍÿ¤] ¾ÿ~çĉG–- Ù½ûRXØ»wqn¬goÔÀw>|˜2pô¡%K–,Ù²eKLLŒÂyÖ€Î52g¯D‚‘‰Ï¤¼(·øŒîã[)ô ÌYÆíçZœ¢°rå1qš¢Ð¸ñ ·wQ zôX7lØÎÉ“._ò×_1wâãSí§ _(`r-ZD["‹Ÿ˜üiýÿ)à•HP/¾ #Ø|c¢#*ü¯Üx¯‹¦(lÚôÏ. 4Eá³ÏæZí¢Ð²å’^½ÖýðÃΩS‡íÝ{922.!Áþ5Þ+ž @ïTÀû`Øõi¶†žÝ'OVÞå1GìRÀ;ëÃSÑ…sçn$&þëÌ8†û @(`&¼ Ö\:•/+?;Ø Y.­¢ÝÙ†@3•lä @(\TÀËÀ“!9Ø>¸X¸ @(̤€7!ÁÉȤ‚™…å–Ým÷ËIÕmÇ€f*ÙÈ+€P ¸¨€7!Íp›¬n¤Í BÏeYQÀÅÂ…àP @(`&¼ &Æ®´²ìž|¥}f*ÊÈ+€P x¦€7!í5ÄÈ~Ù~¹²sQW­/ -ŽiF"mjT ³;Ê,BÛÍ,¾-: HàYáÂÝP @(`&¼ æ^ÞÁˆ,ÁNE& ÌT”‘W( ðLoB‚í¢Yl=K˜€ô²V<@_1—À³²†»¡€PÀÐ xÕ¦³Y,¾Ó0tH"ÀЙƒP @µð2$øîƧæž%À²˜Ã@µËâƒP @C+àeH@+«%¾ËbôÂ|×ϖȆ.ÊÈ€P x¦€—!î}Q×ýÓƒ,¾ò¥*I5C"2€ž+Ü  €æSÀû€¬ù¼˜]´ÆPÙôÛ½Z*#`OT¬] Oô˜¯|#ÇP @(À¬€W"™ïqW—úgÚµûŽ<ŸIyqù¥£ŽxHÀ\¦ @(L©€·"Yp:ͰÝÝÞ´‘#ü‹dûöæøÐˆL˜²|#ÓP @(À¬€#hßé¸äÚê—Ì(-€ÜQ>í‰÷ºˆº© âU 0+Mfdd„††jš$ƒP x£^’­§i‡3/oék4u ½1eQÌþc‘ñÒUÀ€5 ..îÊ•vïnÀ¼!KP @s)à;HÀbô•à ŒV¶ÃÃëU¬8'W®vï½g´¼!?P @Ó)$PÆùU ¡Š÷¶-[ªEY,iKJ• •7d @(`F€r£¯ì§„Oùí·K—¾o±ÿÞ¬PÁ8yCN €&UH Œò«@#ò¬¬¬^íÛ÷.Y2ó¼U¶¬ò†<@(L­@nô•Ý@Ý‹z||ü'5jL)\Xìþý¤L™û÷ïëž=d @(`j€Ê ¿ $з¨_¸p¡ÆSOíÈ“G"ÉÑ50ëõ};H @/PH 7úÊn Ž~ÏîÝÕË–=',x€ËÛ¶m›ŽÙCÒP @/PH Œò«@½ üœ©S? ¸ë€ fæÊ5wν²‡t¡€Þ¡@nô•Ý@íË|vvö€®]»”*•ᘠ6Y,#Ô>{H @(àM ”1@~H qÉOLLlüÎ;ã‹‘ØuŸ¶Xº7k¦qö€PÀËȾ²H eá¿|ùòÛÏ=·9o^» `åyÇbiP­š–ÙCZP @ïSH Œò«@ÍÊÿáC‡j”/¢8X`Eo=õ”fÙCBP @¯TH 7úÊn 6U`ñ¼yïÞv…Þ,_^›ì!( €·*$PÆùU ïZ““3쫯>õ÷Ow‘ Þ ¢S’yçñC(¼X Üè+»\+BJJJ«ºuG+f5"Àøµu™24ý€k9€PÀ»0Œ¹ûÛo×öì¹hèÐM»wßT¶Ë:^ð« ׯ_¯óâ‹kòåcÛ`ýýý:Ä/‡ˆ @(àõ è…C†lèÝ{ÉgŸÍmÙrvÆs4˜'ýuê´tìØ¿v£õ·›4€Su8yòdÍÇ?íçgkèÙ}Æ,Ì)‡ˆ @(à h†ÆmîÛwYçÎóZ·žÝ¨Ñ Ÿ}¶ô·ßö=š`×@kï $àQ#V/_þN`àU×'XÑÂ2‹å÷_å‘CÄ  ðø!Á?nûúë]º,hÛvv“&L à¾ùfÛæÍײ´ÇyŠ@Õ+Ũ!Cš—*•ì1ì·X¾ùâ Õsˆ¡€¾£€ŠH0bÄÎo¾YÕ­ÛÂöíg7mê8bƒþý·mÚt544Sn©5s T¬éééŸ5lø]±b9jð!ÁE‹¥í{縉CD ð55*TP1‡ˆ @(àk 8EqZàçŸÏgŸèªYW%ü—_n\·îrHG6¨[;îÝ»÷A•* p›¬n¬Y¶¬º9DlP @ŸRÀ N VeZ *¶ÞÕHzõÚ°f 6¨^5hFAûO>V´¨•qwïëÇeÊf¨žID ð dÓçpšèªYW%|ëW¯Ž9{6]­1 §J1´oß%J¸±‰±9|Â)“ˆ @(àõ ìÝ{®wïÅÍTÅ 3’îÝ×®\yÑs6ð«ó¦M«ëïϳÕ?/¾uëV~™DÌP @QàôéË#F¬kÝzVýú\ÖºtY}æLš{ý@®uaç¶mÕh-¡Õoö¯³rçž3{6×L"r( €O)²`Á~Ú^ A¯í:øâ Ú3ï‚«l$à]BCC«•/ÔÝ7Y,? È;“ˆ @(à› >5tèê-f5hð¯cŒð“_•Ý¿ÿ²&M¼pFbûöËçÏ?wâD2@¡ ¨{©_§NºuB­  ŒŒ u3ƒØ €PÀ=.^¼=~ü–öíi%£7,flß~Ù·ßn›:õè† ‘çÏcg<÷ …kw]ºDEEy Ÿ›333ß~öÙh<@v¿^™2 4²Ðúƒ†;8ó'OÆ ³­••ˆçñÂ%à9xÑ–l覴´NtÖ¡‡~Ô±#ÆäãÉÔ‹kvíÚuá ',“*&WR„Ï?úh¥´±£FÛ Aîÿ^xv³fJ2F)J€ (J@–ÀäÉ“_¨UKXX(KKëµï¾­5zö¾û²¨¿ËÛow>ð@QŸ°-iiׇۿ`áÂ…Øp’’i(J€ (T“ÀŠ+îlÐ ªéš`AZÚ½õê]}ê©£FŒÀ47bìûí·Ï†[þ©5hžžî°Ä²²2‡)™Œ (J€H) |üÖ[ƒ÷Þ;’µõø:Ìô«UëÜæÍ»uíßׄüüü³5*q£Q£m±Y´>ê¨Xoœé)J€ (J@–Àõgœ±1vì ì— HÛ“…=„¸æ´Óä›b˜ (J€ b•ÀIÍš=_·îìPAXsïäàÐ}öyæÞ{c½q¦§(J€ (YX-8eÊ”Go»­UÓ¦ík׆¿b'V88iPá Z´0çAQ–Ô%@ P”@*H ¢¢bìØ±÷^sMë&M:׬¹"Ø  -­U“&ðs˜ ˆ÷H —\Î …F…BpðùN(ôE(4$ú-Ê …ÊÎ(J €(--òã·^xáEzÀkƒÊp·xyýú3~C‡Å?J€ˆWšаPèÁP¨I(”ùwP(tC(ôM(}q¼5áu”%` lÛ¶­_ïÞ×uÖ•w«QcSÀØ m:ý¾ý6ÀòcÕ(`K`i(t[(´od ˆDg…B#‚}k¬%@ “À¦M›º}þùå'Ÿ|]£Fý«Wc‘ çs>>ðÀv=f쎙1%P¥% ý;Ú;v!áŒPhL•–oŽ ´€ƒâOÞyç¢-nmØpÈÞ{c'DÏm½“ GW¯~ó¸ÜFA{£_Tò`e)H`](ÔÌ< ª‡Bãp¿¬%@ Lp|4zô軯¸â¼FÚתõY}ÒÒF¤¥MMK[œ––—–ëvÌpÀË<°»du(ÀKnNõ…¬±¸/˜x™°‚”% ÀñѤI“~øá‡o»uûà7^}ôÑÇn¹å΋/¾úÔS[}tëæÍ[zèù]Þ°ám >\¿þËuê¼wàÿÝ{ïïÓÒÆ¥¥ÍÚ³±òÕªÝÔº5† t,–HZ <ã#XTpR(T™´âbÅ)J ())Á GLVœ>}úÈ‘#ûõë÷Ù‡v|þùgï¾ûþ«¯¾§M›âb¼íð b‘@úžÁ|ñ­ß·D`QÇ!¡Ðæd‘ëé¥~øaXj‡-Z´€ý-**ŠZ|‰~íµ×öÛo¿H9ƒî¹çž-[¶(YÁ¦×­[׺jß}÷3gŽ’@‰>÷Üs¢ˆ[o½U9Ë(%@ PÉ* â‹ žOV±±Þn$XÆ·iÓ¦&@Y#þíØ±ãÚk¯–Z¬EàÄO´¿×c @¤<òÈ#·nÝ©$ qˆ”Gq„&e¤xœ (€JàÞ„"AÃPˆsª«%#Á 'œpáÿþ]p¶²oqÀÈvá5jŒ?>Rµî½÷^9}Û¶mñ ß0&0f̼×W¯^]$8å”SÊÊÊ”¬žþy‘ Ò»¿<ž°Ï>û`Y½’ £”%@ $«°—±Žô“&%«ðXï¸% #Á Aƒ"åÿ·<ð€lÊ4h`öÇå°þšÃRwïŽ}»Õ¿qãÆ|ðÁ"Y»v픘!pöÙg‹˜Ì $¨¬¬lݺµHðñÇ+ ¥(J ‰%ÿz{íÃÙÇ“X~¬z|pˆVæX‰&¬0;v´zÖYg‰4_~ù¥=uþpD²ƒ:Ûø*)W®\Y»vm+ æ$,\¸PNо}{q9>Rȧ¦(J é%sìƒÑ×Ñ$é¥ÈˆU1!2¿ãŽ;„->ýôÓ•âæÏŸ/Î6jÔhçÎJ9Šo"q·nÝäSVxÈ!"¾_ˆr0“aï½÷¶Nzè¡Øù×~-P”%Ä0±½‘ž-Ib²êqH V$0`€0Ó˜c ”ˆ|qöÝwßUÎ*QÌF‰o»í6å¬}â‰'Dš»ï¾ׯ_ß°aCë`µjÕ¦Ña~XÁñ %@ $µŽÀ( !3©…ÈÊÇ,X‘à?þ6eàŸÿü§8u¾æÀ¦[é›5ƒ ï0Èÿ´ÓNyöèÑãÒK/Q8=s %¿0óN­?n›˜üÏ“w»j ~½æ¼"™%+,]ºTXd8 RnýÌ3Ïgóóó•³ö(HÀJµ„pe`O€#Ë—/‡ƒ#‘LäÙe—ÑAnX‰Uƒ;w:++« ÜoˆA˜Zv?Öƒ{…BÕB¡}B!ì}¼_(„a]laبµg9CÝP¨^(Ô j aÚ@ÓPè°PèðPèˆPèÈPèèPèØPhD µfÒ* X‘@þ¾Î9ç(8æ˜c¬ž«ÿüóOå¬=*¯ذaƒ=uä»ï¾ àÛ䈔!\D‚€? VϬ°,Ö“±Ÿ‘È{4ËWa6cqqqØ y0hj‚”Úê‘ߤìûb(98Œ–––:¯•Ã<ãK†›‚ïø®U®*)¡E$I]$x+ åÆ*»“@LHpçw¿¦ ÍÎÎ/þà_A¬GÀ6ˆÂÖcMAØû¸ùæ›EÌl´Ò<ôÐCâà¿ÿýï°ò $€o=o„ûƒ?‡Ï>û¬wïÞ˜¥‰A|ŠU\999"c¬Kµ_W“ØÉ⥗^ºøâ‹1äN:xd˜ˆÒ¼ysì¥uÿý÷Ïœ9Ó~•8bGXLÔSJàÓò]‰!#Œk5 )2@&¶Ì€k¬Ã?|ÿý÷·juØa‡aù ¼jÀ×nM$V“&MwuÛÍo¿ýÖJvH Kng̘qõÕWŸ|òÉØ17…?|JÃ×7T7ëü¾V¯^ýÑGµiÓ¦I“&ØR7…14ì"zþùç£Í¢&šå<ƒ¶ê ™+÷Ëh‚%pL¢© #Á`ñþK@FxÆÛºü‡®éÜsÏ_ü-CŒ~¸¨¨(RU¯¸â +þ?ùä“‘’õìÙS$Cö¥ïóÏ?iðñB¼CÁ„¡§à¿(R))~VOHIÀ— “N: k:¦NêPb¿üò‹È;_(Wa(I|ëÉì°A¤$À‚ÓK.¹Äžƒu¤U«V`Q¥ö(tìÍ7ß”?EÊ.’Þÿ}{0â’U«VÙÈGļšSO=U>Ž0Ì·øþ%2´àÄ»ÿþʵJ“r{ì1 ì9ˆ#€ ,ø ››p/†W2g4Áx%¡HÐ2ÁwÏâ" 4;!âOvC7A‘j‹wá“=ò·û ÂÔDôQ¢¿‚é·ç6oÞ<¼éXiÐ+ïnéééèÁ¬³ð«öEÕžgªqˆâA €7z¼TFõ)¡Gl~!çi…1D€?å86´‚ÂØŸ‹Œp˜‰©&ò…ö|°Õöɲç#Ž`onŒÈ™ˆ°¬Šâ`ØU´^!Á£>*  ¯ …»ý‡u_ ,?«‰«þDT.¿ür! Q.ÎÝ7qT n?8ñ°B&$à Ptaa¡ÜÿL™2%R}Þzë-Ñ !ñúë¯=—|ðÁdÏb´ÓžF!Ž:ê(‘ìÇ´§éÛ·¯H€oü~j‘ŒxIÄ(øÃòO¼„b|FÈPÀv`,Úž¡8â @†ð;Àøõ×_a¯1ò‘pŒ`/åïAXÆb·æ2`œÊª¶¼„§,w¼ïã{ùåWþ–-[Šq$QO+€A~ܬ|ƒøÑ§OøÜÆœàèÛ¢E‹0Ü„ïPVžþ Æ7€ÇcÆŒÉÈȰ|ƒ£•a®^ü€°Ï—rGˆÂ«§õ9ƺ/€ Æy¾ÿþ{ìŠ 6²‚”°0çºë®³r#ØÅô#/% 0²}yÐ…ÇúÅ!çH€ÌñÂ"úÕÿüç?šâ0·P¤Ô€aM9>Xˆ«»„†ÿ{àD²¶mÛ†O”ÂGe$ˆôí{ݺu°³X*"l%R|²×Œë‘^#ð•¶[#û÷Þ{O<»wÞyGI)#• ~±ìc°ãÂ;’Ù󱲕Z5a‚•âä(xìøÄ/´Â^¼úê«X˳dÉ{âÀI||¹ñÆÅq+~8á„„1‚Q5%ù|úé§`ù æ(]&:ÏÆAÓ$VÅO Ä„sæÌ½PØ>S®9úU çŠôJßRÑÛ¿) Ìþ‰ñÖz{’saà„‹AÌ`´ÎBΗõÉ™ˆ°ŸH€´ã?Þª9fÒÊMðÅAÜoÔÁ:QùH"A$Éè8>ëc -n+ïüÂÃC¡uºoVÅ 8G|Ž[ GÒÏ%ðÿFX¢]ñ!&éay»0:øêmÏÙ%`¿c1i0¬.ÉH€°½Êa÷±šR>5yòdq#ž’OÅö àÐ 9EÍ1¶&×3Å©Ž;ʧâ âZ.÷©³ SAPhYîŒEJ‘`úôéçwžèˆ»v,P·ÆÊć¦yˆg}ôÑGÛ%dffâ#8–!ÈËT±†„ !#––(gíQTÒª0r“Ï⣒¸‘°®äÄQæ‘S=13:¯ʾŸ/¼ð‚¸),ä‰Zm}"^>: G­ÿ2FLJBa\ŽèîY$ #¼ <ò¿p[„a[eº#LP÷§z,ÅâFxþFûÚçH€Á%ø2‚ÂÈKDæ`¬R´ß£ŒxÓ·'PŽÈŽŒd·Wp $ÊÂÊ>Ì\uþïÙJ)&c2Xc§^òœ¨3¨¶R yèÛo¿íüŽl¯äF$Pô(¦Ãìã5Ü Ñ1|мOõ“‘@îˆ4áýë_r¯ëSEYLìˆ PTÆ …Ø}X9A¬VÀÌRc½3Î8#ÒÒ9 /^õÖ±9—¨-\‹ô°˜âx¬LøùXo‘³p›|p¤ŠaÁΰaÔ: Šé’¨ÇíE‰v ý6™9Å#*¨‹%^A¿]õÃk©¦Gåüãâr€ð“ž\³f’UبŒNö°wZ³fM9Ã~ýú‰jà㦠ÀòÄrKüAÿ¿Ùó7\øÃEÂ[F䬖‘Ž•³JT8»\ÒC víÚ(Ãt yq’§}ùå—ŵh›ÖMáŽÂÞînEzxUòH€4XÒ«œe4ÐØ aÑí5¡ÐþŽÙ`¯Pž ÿ?–è[Œ£rv ‰‰IÈPu,|Ö¼’ÄQ(/¡‚/a(Ñ"9ŠtavľºDÇ×¢éav\T3§TCFìU¤œU¢hÚb=£29_^¯ÖwŸ’|þÇGöø2„ÝBHÎM®¤åã£ËòÁLB¬ € b%& Çâr¼øhRZ§æÎ+Òùå—Jz \º8PrfÔ? ì…F…Bp_pf(tèÿN6Ô…°‡¯ÅUn¡`úö2ý·XÕ«waê߃cI”€V !V$xæ™gD;ÂH‘Rk=À›±¸T)×:‰ÊH€)úK0Æ.Š{â‰'äÄ@ì¡`Å,YùTذl=ñή¤‘‘ }ûöÊY9ŠrÅ$ û.Øa\TØÉz 9g„±/¤¸¾£•³ö(vséíƒ2DòmÏ“G-|VØ´gE!Æä*]ÓX+å·`~ìðk¹LO T ¸A,6M £ÓŠ4ôH„°®ÅEåB‡Q Õ¤I“4â5\TuìØ±JJ@‚8‹j+g•èÓO?-ÛGÑe$¨W¯žyyyÀ-ýåÖYÍdH ð•'’E"¬N •€¼Šõv˜«{°{ˆÑš3sJ Ȉ 0Î/öàC[ÃÛ¢ý£".Á;¼¨Š5}a›­õÂnÏ_%ÀxÍÇ[y¤¬0R3ÓOd^YY‰wg ¨`Œ5Ÿ9$@ÎX(V(7ˆ­B7lØ J·pߢ~'…gûµâPæCJé" À¢E‘Þ‘gá€ÞHÄå"ß4H{qØÝŒT%qñ^('ˆ 0®ŽÉíb&<ÆØ¾¥:© ÓP”%àP………Q¿©9º/v(v&«Úpˆðuƒ¹årb´5´GL}¯ÚòáÝQ”@`%€mVäu@Î@I‰!º/ìSfÅü”€låá<NÅVáÝpà ^xá!‡¢´ DÕXȦ©*¼úˆ­²~ÿ{µò (/%°`Á÷ß0Šî‹½|*Ì+™% #Ýî‡=‚Á˜¯qžcÉÃÉŠƒd–ëN P‰—v ÛM9?¨à,‚Ä?HÖ pŽøL€9êðfuÖ½ugD‚`Ù"ÕŠÇ)J€ (JÀ Àa†ý…7ÀÖ¥Ö”xUÅþeœràÿSf‰”%@ P”@T Ìœ9öÚ Øó{|ðÁX§pÝu×aPýfjQk^e`þ$áLëÓÅö`iÖ¬6IÁf©Ø‚ÍáÍâC3üÉÃ麵ß%\Oà4&x|õÕWÂK|¤¬0í ÍXaw]ÁζŸ^÷#åc0`€•¸K—.ú”]£30èXצɪI“&Xc¢Éû°ˆËÃzÎÁ“ æÎ«É §Ú´ic%Æ.óú”)ø²Ö·6xð`'w³÷w / 0¤ rkРÆ[>¦tŠ”Ï?ÿ¼½P"]&N®@Ãò ÿm·Ý澪7¶‹gvæ¡\¨O<<&ùÂDE ŒR”%Ê8ì°Ã„ÉÀꀬ¬,‡"ÊÌÌÄ>/ønÀ…a3Œõ –ãá«úæÍ›Ö!PÉ0iPÜ/ðfùòå.«‡)"CLðˆšÛG}$ÒöÙgJz""F}“¾‹Mœ8þÕ1¯ÎÓ@Îðˆ~å•WÞsÏ=ß|óMØ©/¾ÕQ)+ìÖª·Ý˜'‡pÖ®]‹œ1lu BجHdÒ²eË;vÄQ“Ä^Ò®];qSèîÜWfÊ”)"CLQˆš!æ-ˆôÏ<󌒞H „Q$€¹Íún“”n½õÖŒŒ êÃR(JÀ’&§iÚ&¬9¸‘Ü©¬!>(¸Yª×|—\r‰}Jƒ›ºùpí¹çž+,2Ü º/ñ§Ÿ~‚7¢f(SÜ}÷ÝJz""Fý‘Àe—]&ÔXÀLãqãÆùS%–B P–yä‘H“A Xbà‰ °d®wo¿ýv`þâXª€Ù†XXçIeüÉ·Œ½DÕûŸ“Zav¢Èðã?Žz >Uˆôp”¤¤'(aÔ XH€Y1O=õÔ_|1f̸,î~øá‡˜=‹o…Biñ.À± K¡, `@ØÕˆ°ÚN†¦c#Þô1ú}ÕUWiF'D‡ À/¾øb¬%&*=fEÊõ/++s_¬y¾ð Q3”W,ÞÿýJz""Fý‘ÀÛo¿¥4Xµ¶8àæ=?ï¼óÂ&ãAJ€0$y|[´DŒócØÙP‰È(ÇA+èÚµ«¹Zy˜ó‚ „0q§žäŒDžðs5OÙèÛB>uƒÑ£Gë‹»úê«­ºaΪ>%ÏRz €Ÿ±ÇºPuûbýå"ÀóœôÙ¥ÉÊ•+…EFÀIŸæD°‹-Ùâ‘avhØ«`#Î8ã ‘²S§Nöd1!²ÒL7Å´OQV—.]ìeUù#˜,‡,›4i"ä);õØcÁúÛe2}útq•ýC’³qDâ‚‚å,¢€ëQÍÜO<42»vëÖÍžÏ?ü ÛØ“YGÚ¶mk%F¹‘Òà8šÃ'Ÿ|bˆàd¹&ƒ' Q®ÞYn‹”EâHdw©³!»Fþû¬½&òÂXœÅö¦³gÏÉüA8âVöﰘȄ1@0ƒ8Ž ÁY‰(öþ£1"=bØJ>ˆ°Ý§ÈŠJ€@´¾Ô©SÇ«WZ‘¹ÃÀ Aƒ„qQrØ(FO:é$ͨ©Ãr %3îPyt\^•‚!±u2rFxï½÷þ÷¿ÿÅ †@öSŽq„ a‹Ž 0ÅËzÈüÉ'ŸìÞ½;FÈñ^ ´òæY¨IزªðAÈ[ƒÉú‰„ý,  VÏœ9̆í>øo²²EÆUŠXrøá‡vÐ&’\Ç(´üÅ VÓr4Û€sp;i†E‚;î¸C<TÉ~›ð,ŠÍD+àÈZýìÛ·¯R·O?ýÔ^“ßÂ"ØFÀƬzõC«±hV)‘QJ€+Ë%¾ûÝyÂfîü æªa!›<ÝQ鸔(RÂ3³óü}K‰÷GQU@&RzX4¾`Ú˜ü&(ʲ˜á¦_Ð+ òppéã†D‚<µÃCÉ‹¬ð åè£bÇÐCW{xu…?£Hpâ‰'ZCSúóÏ?E•‰ú#` òòòÄ8 ,>†2”úXQÌŒ•?º¡ba‘@¸Â@Ì@›R”@ÜÀ) 0Æ}¹‡Æ4᳆0~èaéf…}£D—‹Ieæle…é…X.zÍ5×  écíÆ…^ˆqLG×ëZ¼4AnÖÃ^7|"ÿû|/ñþ…÷ÜÏ?ÿÓA±†@ “áÇG-Ξ²Áô<ñp1iJß¾Íaš½’Þ«ø: ,/¾ì(¥ÈQeq ˜Fl}(гïÃ%W ÞËEJìH ϳÅl zÈ—3L PžH 8¯Û01b•å.B c¾'·ïy&UÅ7w|ÖŒ©LHø MLNĘä/˜`r…û{÷ Àöb‚†ÓõÃò"¡¢¦‘Èj•bÁˆ¦b˜Ú$j…€ Þ{ï=‘À“=Å4•á)J€ˆ°Ñ*À·ƒ˜ÞÑ|»;€M›6mDß…õépaä°tÌOÃüÉY³f9LÏd~JëîÅc…SOI¼BÈAxŽÂ0Ž^,òÇ}£H€× ñ 3 õµÂÙÓN;MHØŽØ_UœÅ¤‚¨¹1%@ T àƒxÔ ‡˜e„eÚÁ¼_,Ä»è¾PU ŸêGcà Y¬Ò"ó±Êž!½rÅà!à{¥r˜Ä@b~ˆPN£H ¯vtòíæ›o³#Xõ‰…%šä)J€¨bÀRwyj±è(ä|2{ò¦fBtË–-“©Õ®W¯œº`Í ^pàµ&??ßF±;<߉‰aÖÝ™C ìdý9œgB8ɘ'&Êóß²³³=¹ ‘j#¾` Ó?0§1l%åéF‘ê-ì»ï¾¶2òAÙU‘ ÄrEl ‚¸1ýé¿YÈu`˜ ‚)Ë«èRì$À¶ËÁ¬éíHàä{¢¸\ üüóÏöâx„ ’HQ‘ïDðä;Â$s8,’ý)=•Åæ°èŸíxxƒD‚¸… ‡üâa]tÑEqç£\(#ŠPÎ*Ѩ‘sðœ/æÅE‚#F¡9ñ$ß  08 r‹5à|>"sF)J ˆŠèÐûÖ“¡#¼· 6 »a÷yðõÇÇ8ÀÒ6'KE†qˆq‹Î„ºé¦›âÎG¹PF‚°ÛRÈéåia÷8°c»LÌ]Á×(EÙDýåve’‹°ÂòQpbÂÊk ”¬àÅQ”åÄš˜«ìH€ýŒ¬ÜÐê±T¶çž?TXý÷ŒøVrð‡™ºð«)JçtDåÑ0J $œ 8 νcò&â;>6˜CÀ蘀ý®áذáßæf,ØË­G°¥þì€Ľ„}}–‘Sô¹ ß\˜OkO‰YµVYø|ç öâȶmÛ°ÿš¨˜ 0 ÎbÑ„¸0RÅ­ô àHixœ ’E‘ iÓ¦ÉrG¬gòJ@ö“sË-·xu#2À~a-j¤œ1/ELJÁz¤dNŽc ß²•ج!ìj> °¤qÉ’%‘²ÅÒZxÞ°rÃnJödò&ž}úô±'Gà“ÓÊÇúoG|+Ó$°bW¿S&÷ŠÄÉ…¨”%L ˆ-wö0¼ì³þ¬UU’>O Ý kûâ»Y  Ì‘,ìÓͶ_pˆ-Œøå—_¶Ú2à®#%õü± Ö;Øs“wýÉ`^= Ž`ˆ@™=hG${ôÑGEqo¾ùfج¬ƒòD‚X=ˆj²å)J€H”"v“IT YnêH@ž<ãåÕ+H{wþùçÛÝrÂÙ¯ìæ›…­¸;c£Š°gqv{‹ «iÓL {Û7Ř6mšø.€4Ø#Ò^.&Ïœ{î¹¢ÄV­Záý]I–““#×ÊJ ài¡~ýúVLžÄ ûœ ìüâ‹/  Q}9*•a” ‚)‡H ßä%˜·ÆZ%° ”°20I^¹¾‘‘àᇶ¶]†æc&!| M˜0ƒípà#ÌQ4¾ÝGš•Šm•,sÙºukL Ĭ{À,5^“áØ.ª„{a$Ã\¾HùÈH€å0ÖU°Åð@ˆ|ðâþpn ohoÑžéÒ¥Ke·c‡z(–N±0^&ù¶¸¥è½Zù+{&yä‘.€t¸<=z4$€‘Kø=)4€©Â¦Ø]rÉ%Ø´ñˆ#ŽÀ'ud‚÷}ø ˆšÉªU«à ÈúŸH°¸ß °&8„ZáÿwÞ‰a ýÒB¹,¤„o\…Ê l#uà 7àë†à òÿ]l¯-[¶È׆ EàB k3­*aè$€¯3fÌ›ž)J y%à ðæ¿É{¬yrI@žÇŽ]·6nÜSý­waù’°H '`˜ (Jp‚ðmÂáAj‹oÀ¬û&MšˆŒ„¯]»ÖIé˜Gvÿ~D'ÒcJ€ œ fÛ×4Qt”€9 Ào¿<óçñî¯Ù¤£åX‡Z "¹çœ)J jKÀ `m²óO™U[\¼;ß$ðý÷ß+óçñŽzƇ1+Ìä‡G}¬‰+k÷/3s]z:~k—-Ë[²d÷oñâÕ‹­^¸¿ÜùóWÍŸ¿rÞ¼•sçæà7{öî߬YÙ¿ÿŽßò™3—Ϙ±ü·ß²ð›6-sÚ´Œ©S3¦LIÇoҤݿ‰—þúëÒ_~ÁoÉ„ »?ÿ¼düøEøû×oôè…£G/ÀoäÈ£Fá7ĈùÙÙ[ ÊJJ*M–yR&$°ysñ×_ÿÒ£Çä¾}§øûÀ³ú÷ÿ}À€YàÿL„û÷ŸÙ¯ßïÿföí»û×§Ï_ÿûô™Ñ»7¢ø¿;Ы—õ›±'€ÿ3zöœÙ³§üÿ¯hüOGl¿ß{ôø½gÏYÝ»ãÿîp³öüf÷ì9Û #°ç7§gÏ9½zYÿqá¹{~ózõ²~s{÷ž‡_Ÿ>à×»÷ü=ùC†dNž¼~Ö¬M‹._¾mݺҭ[Ë+*þ4!jæI ¤šºvÿúë£Ûµ†ß›oŽüðÃ_»wŸ3thö°a9)õ>|å¸q«'O^7sfÁÂ…[²²Š×¬)ݲ¥¼¼|Wª©ï7àÈÊZ÷Î;“¬_ÇŽÚ·Ñ®ÝÐö퇿ýö˜O?ڷj¹âfGÊ0aÍôéæÎݼtiѪU%7–nß^WžÿëÞ}R—.c¿øb·ßNòýÍ~@Aü¬ðÿ ßßdøøía<»yà·‡û…¿=¼·›úþ¿ðÔg!_ß¾óûö]ð÷oa¿~ úõ[Ø¿ÿ¢=¿Å,8ÿ— ˆßRü¾û.ýûï—}ÿ=þ§œ1hPæàÁ™?üeý2${ôèÕ¿þºnÆŒ üQ˜ž^´zuɦMe¥¥| ß,>ýt¼è^D S§__}Ô+¯ {õUp¨>šØ½ûÜ¡CWˆÖ—R««™6mÜ9›–,ÙºbÅöüüÒâbt5)ßׄ×)5+ D›•;þlqÂk¯íæ„Ï>›Šî4¥Ú¬|³ ý±có&N\?cÆÆùó 32¶ååaÀ³lçÎT¡ý7Þ)ÔCbÈa{rJÊ2äO?­?>#N¿ÿ^°hÑŒ8­]»cË–Šq ‹Bä8áµ×Fa0áÕW‡¾õÖ¨?žØ£Ç¼”åô9Çç€?'N´øssFFQnînþܹ“üiÖ&¦xîQ‘@n¶"üÖ[ã_}õ'Œ'€:wûÙgÓðª%›Î [FaÊ|†Øm²³‹×­ÛÏøZ•tLF¡r@0dûö»9¡k×)ýú-NA}·,^­§œœqr޲ ‰ðN N€.íá„I={þ‘Êœ`)•àÏ=_<·Z_<‹ŠR?«R7›À{‰ D›•ÿË ã>ÿ|ú€KEǘÊÐþ/¿¬›>=Þ¼ÍË–­\¹}Ƙt”ŒCƒQ‘@V ~ë­ÿkêÔil×®`È”æ41â4sæî§ÌÌ¿FœÊʪΈ“K$ú#Þ~û—×^' Åw‡ŽGüñ¤^½æ“¬vÔ(t5kÑÕ€?“½«I YLå¢=D¹ÙŠð›oþ5žðúë?uî<î‹/~8pY*ã|ï }|†˜4 “Ž6.Xð—Q(,,²Qˆ „>È‹!_y寱¦= ™êcM–zŒ™‹ÏS¦äÏž]°xñî§õëK·m+ßµ+ÉFœL ¬B" Nhß~7'`<œÐ¥ËäÞ½Èm-Åúš² w5©l—x撚@´Y9ðæ›ã^}u8Ú/8áwvs¦u¥x›Unß2 S§æÏ™³Û(¬X£à!È*!Âö±&2¤¬ÖüFL|7ó·bâkAÁî§ö!š¢}C¡?rÀšëæÜà c>ùdJŸ> ea2 ìéjÖ «.Y‚®fûúõXZ‘tü©ÑCžr.„ ÜlEø7dNÿå—3È a»,É(l‚QÈÍݾq£kŸM#Ð9 3¤5ÖDÝPtcÏ27kÄé¯en{æ7&x™[b‘@V!–æÄVýuU˜£øÓO«FŽ\…Nc̘ÕcÇ®?~ÍÏ?¯›0aý¯¿â·aÒ¤ S¦lœ2¥`Ú´MÓ§oúí·Í3fΜ¹å÷ß·Ìž½uþüâ¥Kw,_^ž›[¹y³s«Â”I,à h³rà7ÆŠñ„wßýù«¯f”¡t†¦£V³1b¾ÓYÍjÜ8´¬µ&¬ÛÓ¬ò'NÜ0y2ZVÁÔ©j³š5kë¬YEsæÍ»mîÜâyóŠÿøcûüùø•,XP²páü-*]¼x'~K–”á·ti9~Ë–Uà—žný*ÓÓ+32ðÛ%ýþÌÈ¡ÌÌPi©qULÈ*!Â2'¼ûî¸/¿ÄX“ߺaZ÷<ɯ?ÿ¼ÆZæ¶téÖœœbLeÙ³¢Öøgˆ"Ð9 æÄÊëª0?¿!C–[«PÑó`êwßa±êîU«˜ Ó¿ÿâ=kZ­Å­X늯–ÓÝÎOúöýcÆŒÍsæüÃm_4|7m?3S4ÿPF†úKOyûËÉ1Þ·°€ H àH 7['´k·û»ÃGýÚ¿ÿ‚ð[8pàÂï¾[ôý÷‹ñßãCíjt’˜‡ƒ+VãÍÊÛFjÏmÇã:$ú CvèðÓ†œaš!ñ¶s÷¼ˆåþy-^Ä~ùe=«­±©SÿÿElæÌÿ›=ĨXݸ¸`Á_¸hY%KvãÒ¥ÿCŒ{ q7.î!F .Âv„ÑíµkkK² ¬B"¼pa™½‰¥ì"ñÖŒ’ D›¤l {ãD¡rÀbH|P.´ÄÂÅü ‡_þÓOÙ#G®=:g̘UãÆížÚGUpI4iR>†U1¦ŠUŒ¦Î™³]ØÖ°òO®ƒDYIìa"¬ÏD‚`Xlãµ ÈjŸìa"½cGà1/ÙŸ¯·õ'Ý ÈúF$0nŒƒQ‘@Vûd ÂöíÖA"¢ÞD¶à‘@V"A0,¶ñZ dµOö0‘@ÓÉ õ&h´…H h ‘À¸1FDEó“:J$ÐtòDE·‰m!(ÚB$†Å6^ "¢ùI%h:y"¢ÛD¶ m!7ÆÁ(€H h~RG‰šNžH è6‘@£-DE[ˆÁ°ØÆkA$P4?©£DM'O$Pt›H Ñ"¢-DãÆ8 ÍOê(‘@ÓÉ Ý&h´…H h ‘ Ûx-ˆŠæ'u”H é䉊n 4ÚB$P´…H`Ü£"¢ùI%h:y"¢ÛD¶ m!Ãb¯‘@Ñü¤Ž 4<‘@Ñm"F[ˆж Œã`@$P4?©£DM'O$Pt›H Ñ"¢-D‚`Xlãµ (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"qcŒˆŠæ'u”H é䉊n 4ÚB$P´…H ‹m¼DEó“:J$ÐtòDE·‰m!(ÚB$0nŒƒQ‘@Ñü°ÑEe½ryvÃ;wm~òÊ¢ž¾½uóÒcO*9ã¢m×ÞZøÐc_ÿ`MÿYa¯õó ‘@ÓÉ' &fç¾½îÛ‡ Úݼåþó‹¯:®äÔ#K;kûEm¶Þqï¦g_ÌÿpЪߗ¦ïòSO¬²ˆm!( I$†Å6^ "¢ùrtfÖ¦Nëº_VtÓ5Óv¥é{ïÚû”íç<µ±ãˆœÅr&~†‰šNÞg$°júƒ/³ã$½ÚXg)¯ý–»»¬ù~~f‰o C$Ðh ‘@ÑC"qcŒˆŠæ[ÑYY…¼z@ÅANús{šK·Ý0,gAØœ$h:yß gî„S··²k…“#u˶[ÿ邌R£zbeN$Ðh ‘@Ñ@"A0,¶ñZ Íÿ#sûã;Tq°“\“f¯]{]UtëØ™JþF£DM'ï`dàôâó5ZáðTò¦o¬û¯é¯ D¶ ”žŠH`Ü£"¬ùcVdµã‡ý¶“dûWøášrFÃDM'o –¥ÿ‰oFøxäD+¦9·ør£Tˆmñ ¦/ÏFvÍûñµõŸ?ZÐþžÍO?³¡sçµ=»å޶r>¦0í.bÊœH ‹m¼DÑ.>ÍûÁÉœ‡ý¹œìŽÍûÓº‰šNÞà3ÓÛ®‘Ÿ¸WáFe‡~·j†PQoD¶˜C‚%ýs§bº)&šb,Q£*øpyɶë1^ôkö*o}¹ Œã`@$°Z^ñ4mÓý)Ì<œ›¹-Ž–Ó%DM'o ° àÐG¸×H9T¯ÜÇÐ@‘@£-&sG_ÈÿSI#=kÍq|ê›;9¦ÞÀÛÄD‚`Xlãµ  á¼µ®›¦1zuªuñ•xAð¶*¹ 4¼ $˜µÅÛ/Ma• T€5°Ê³v%h´Å[$X˜±óÕõ]ë•5 û|<§øRs£Fz"7ÆÁ(€HðõêQÕvUsÞ*ݤÄÊt}»sy–H éä=G| :³øB7úàüZÌwž³Ð¥z(— 4Úâ!ŒÊY OΟuÔ”wnþ?"e…!Ãb¯EŠ#ÁÈœ%˜µ z˜à¹ ïÊ ÍÛ0‘@ÓÉ{Ž7n¹×Cňš–!x;ÛH Ñ¯¾&â^ˬQ |ˆœ´<ÏÛÞCŸ‘À¸1F)Žgo¿XÓîLœªQ¹Ÿ¹ÉBDM'ï-àî õÐçy{á£ú~;¦³D¶¸G,B¹oÓsúêæ,æ$ô_5-¦'î&1‘ Ûx-R °öÇM“ŒûZ¸DvÓ65× 4¼‡HÇí8%nˆûB|áòðó‘@£-î‘k â~Ð/¬YQë§œEšÁÃSDãÆ8¤,ÀAœÑ‰âúFmhò0‘@ÓÉ{ˆþÌG «B×òªŸ'h´Å%`å`ØÇçùA,Sõç ‘ Ûx-R °çÍÓy†˜9ìUÇ.çC$Ðtò^!„Ñ;Öž§ôj¸˜H Ñ7HàçŒeh×±;Nöa3‘À¸1F)‹­Š/ó¼¯vž!F€½*f‘@ÓÉ{…p7çüA›H w2Æ&h´%n$€uv¿Ø0VµÁ¤…¸ÕÀá…D‚`XlãµHM$ø-k£o #µn S8lŒÎ“ 4¼WHp÷æ§"=SŽcŒ#ε"RJ"F[âF‚¶/ù£r)ð\gì‘´'ljÆq0 HM$xsÝ×rƒJHž‹$¸fëí~ªR–¡IËVC$0nŒƒQ@j"ÁiÛÏUZSB¢ø~!t÷a"¦“÷ zäþœUQ ÅFZîµ…H Ñ–8`^fñ~•û+OÊÏèm…»×ŠH9 ‚a±×"5‘ ñÎf~6ÕHe ]ùG¤ßq"¦“÷ °qm¤§éçqlŸ†ÈW 4Ú|´f Ÿ:`/ ΋–¤WÊØÃ0‘À¸1F)ˆøŒÙ8öåÿ‘¯Vð°Í"+"¦“÷ Lï˜éP O(ù‡{Í!h´%$¸´èF‡Ï\²Þ«&ºWŒ°9 ‚a±×"‘»×yÒ$±×9–-€.0¡ö«8Î̬¨ —b—ש]^·Ny½ºå °" Òà ¾ÑÎÚì<þ‘Ûyd³Ò£›—ûYÞ°­/îƒDM'ï ` 'ʳ÷®½«UV‡òì[Y£ÍØhʃ½j–×®U~”o|uËÖ/k åÁŒk5ÝÙ|·æì<êðÒcÎÚ~QÜJ".$h´%V$À&§è<Ñ 7™Ü¿éyñ|½  Œã`‚H€I}‰š¬4vŽèûdoÏz‚Ooè¤<Ä„D9Jà­n„ÍmáÂ2çVuòò5 Ñ¥ÐË‹nv^ç˜R ‚a±×"‘ /\JSJHtX΂˜ZeÔÄ%Û·[=A‚w×öNˆª(…bŒ:ª2DMÀQ¶àTLH0xålå%$ڲ䬨Ï=¾DãÆ8¤&œ¾½uB¬RèïY›ãkž‘®"h:yO Wî/ÊCLHô®ÍOFÒçljm‰ ¾È–MP Åg&ç SJ"A0,¶ñZ¤&\»õN¥)ùŷ㘚¤“ÄDM'ï Œ[‘忪ØK|)ÿ#'ú OC$ÐhK¬HàÕ>GÖ$kzf˜Ès“ÄôkbÒ_sKöÌJ:²ô¸£vœpÌŽ“Ž/9Íso'– Œã`šH„áß ·µÑ÷Øqœ%h:yO%_~X9'õP.!h´%V$ø`M;¹ùSU•§ìU”H ‹m¼©‰Xtðuˆï¯íëUkù 4¼WHÐvÓ‹þwõr‰Xw ž¸›‘@£-±"VÿÉÏ(Qa¬Fq£šk‰Æq0 HM$€æÃ%x¢š-ÊÅÒ³9™Ešß)"¦“÷ >‘ì¡‚WâSå*"F[bEì:”ÀþD}öö‹•§ìU”H ‹m¼)‹ï­í#Ú‘ÿK·ÝàUS•ó!h:y¯Ç{ºÿ:#J¶r¾üÐã 4Ú+`Odñ€¸~ËÝqëƒþB"qcŒR g”cNNB/|yò-ØÞ„‰šNÞC$xM¿„h ½xÛuöçß"F[bE<‚Dõ'²*¾’ß%>eˆz‘ Ûx-R оÍ'·&ß·ly jŒ/‘@ÓÉ{ˆ˜Ô}êöV¾)Œ(ѱýn|ºa¿ŠH Ñ–8àñÄ“JHïð˜dО!7ÆÁ( •‘-ï\>7^8­¾<ß“FjÏ„H éä=DHã<è}Vž ^¶?ô¸ 4ÚŒÌYâ³>(ÅQ|AÜÊõB"A0,¶ñZ¤8LÈÎKy¥ebýrÔÖw"¦“÷ ðŒîÝô¬QUQ2ǾØ~7nݰ_H$ÐhKH Ã9€òÔüŒvX÷•ý){u„H`Ü£€G´—þ«¦aþ¿?-÷¾MÏyÕBÃæC$Ðtòž#¶ÔÄ&ÅþhöÏŸö¡Ç}H Ñ–ø ÓÚþ能l²†µÕq+CÔ ‰Á°ØÆkA$@[è’7ȇAàk¶Þnȱ˜hÎDM'ï9@ìó3KN.9ÓÞ?{{^ìLÌG%h´%>$%žXrº·Oßano®ûZô&DãÆ8 ¬æóêú®p%ê°õÅ‘ neݱZ|šH éäM f†½ãÄ8TÂá%àorÇÆ§ú«ˆm‰ ð«¦;|²&;®äTЈþq»b 4Úâ ðŒfd`…ˆË>ÄÉåm ^òP%4Y Œã`@$Û 0mì…ü0‰×I«TÒn-|è×ìUas6zH éä}@ëáb*àÅW+Zá0 €kÄ%é•FõÄÊœH Ñ—H c…ˆû7 ½Ú\¿õ.Ó3–… ‚a±×‚H tÞÀ7â®y?Þ¼åþzeômg±-)ö;†ÛŸGäj 4¼oH`=Ìxnû§oo]mWµ¨Êƒ9ðrÐ+÷`Àª!‘@£-î‘Bž´<Ï—K?g(YÚB$0nŒƒQ‘ÀRxý ø+çÂÈc_ÿgáƒm»¶eÉY­Š/Ã<"l•‹ ÝrÇà¢>Î 4¼ÏH ÷¬¬ÂOó~xaÃû0úXˆzÖö‹`&.-ºñŽÂÇžÚØ±ÓºîãVd‰Ä~ˆmñ ð4±—Ê=›ŸŽÊ„1%€w5t8~ª Ê"Ãb¯‘Àç–e´8"¦“O}ân2'h´Å+$°ÐyÃ/=&&»)1ÞG~É^éæ¹Çw-‘À¸1FD‚øH0¯"h:y"¢´D¶x‹ü’ŒŠÎk{6ÝÙ<’­zß%Mx¬R´"R”H ‹m¼D‚HM  4<‘@Qi"F[·øòWò»Œ^‘®ÉÇÜ)"¦“÷ &dçܼåþj•Õ qmYrÖ·«Ç™Ó9g"F[ˆ²ª L$0nŒƒQ‘@Ñ|{ô·¬¼|ØÎ#té"MÓÍØô^íš;B$Ðtò> ÁÄå¹·>T½r¡ñþ±ý¼>¹“Ì鉕3‘@£-DEýˆÁ°ØÆkA$P4_Žâ#ïC¯ì_q`|;®Ú¯ò€¶/ùö±˜H éäM#A—5ƒ¨8(nU±_xÇæÇe”É ém˜H Ñ"¢lDãÆ8 ÍÑöë?;°¢¦½£Žãòyu}W‘³¹‘@ÓÉ›C‚%˜7‡bD½ß0ò`Haˆm!(ZG$†Å6^ "¢ùˆÎÏ(i³õލÝu¬ ®Þú¯y™Åöâ<²ô87ý¶Ãk›í< «D¹ˆšNÞs$x|c‡OÜ“dÇí8e~Ƶ…H Ñ"¢iD‚`Xlãµ Xš¿0c§Ñ/ŠQhYr6f,(Î}”H éä½E‚9‹í¾)”§ìyô?ßp¯$""F[ˆBO¬‘À¸1FDKáïÝô¬ç¸>Ã;6?¦4:÷Q"¦“÷ Z_¦¾&ÎîW¹ÿ¤å«Ý뉕‘@£-DE͈Á°ØÆkA$€æÃÙ {÷2±š¼cÏY¨´;—Q"¦“÷ ¾Èëãö*=¶Hp©$âr"F[ˆBO¬‘À¸1FD(üùÛ®òªÇŽ)Ÿ³‹/VÚË(‘@ÓÉ{ˆG—žÓƒö6ñO9‹\ê‰u9‘@£-DELjÁ°ØÆkA$è¶zŒ·=vL¹}ž7Tizn¢DM'ïÀ"Çôˆ=OüØÆ×Ý(‰¸–H Ñ"Ð+@$0nŒƒQ‘àÒ¢<ï´gˆ ¥é¹‰ 4¼WH‹ìüùšHyTéñn”D\K$Ðh ‘@è‰ Ãb¯EŠ#\Ïí[YÃD¿í0OÌa˜¹Ei}qG‰šNÞ+$ðg¥ª^F­X·’ˆ ‰m!=±DãÆ8¤8¼¿¶Ÿ¾ïõáì»k{+­/î(‘@ÓÉ{‚pfåƒJD-pÇ­$âB"F[ˆBO¬‘ Ûx-R ûÕÀêù/)º^i}qG‰šNÞ$ÀžÅQíµ îÙôtÜJ".$h´…H ôÄ  Œã`âHG‚>tàú"P¥õÅ%h:yO ÃJP§+Šn‰[IÄ…D¶ „žX"A0,¶ñZ¤8Ô¨ÜOo¯}8‹:(­/î(‘@ÓÉ{‚ÏmxוˆZ¼_Æ­$âB"F[ˆBO¬‘À¸1F©Œ3² ¢ö½þ$@M”_”H éä=AŒØ{¢{ïÚ»ZeuL.ÅìVø$Ü¿âÀ*:¨â`lzX«ülˆ|Hyýºå ë—5nPÖ¤QÙ¡Øö¨éÎæ‡í<cJ‡—ã‰G "F[ˆJD$†Å6^‹TF‚a9ó=éÞÝg‚š( 0¾(‘@ÓÉ{‚Ol|ÓýãvŸöïŽOC䫈m!Ȫ‚0‘À¸1F©Œ?®œë¾sF{íÚ Þ‰­7>|دò€¿ßøj\^çï7¾ØvÙzãkTvX“‡ºó¼ñ5ÛytóÒc±‡ŽÒã‹ 4¼'HÐi]wOtÆe&nkŸ†ÈW 4ÚB$Ua"A0,¶ñZ¤2LÏÊwÙ3{uùÔåë”_”H éä=A‚oVõê¡»ÉÇ“=³ˆm!(]‘À¸1F©ŒËÒÿô·#»!اr_ÔDi€ñE‰šNÞ$ÀxŽý ú³ãÓù*"F[ˆ²ª L$†Å6^‹TFè9¦lùߟ+%bΘÒú⎠4¼'H°$£¢Ny=å úí;1n% 4ÚB$zbˆÆq0 Hq$HÔˆ²i½íJ¥õÅ%h:yOæ¶Â‡åÇç+–¦ïŠ[IÄ…D¶ „žX"A0,¶ñZ¤8¼½î[ÿ»t¥Äë¿RZ_ÜQ"¦“÷ zåþ¢`´?·g~ý–»”Fç>J$Ðtòž#ÁÜÌmÇí8ÕþdM£ì«¦»×9"F[ˆ²ª L$0nŒƒQ‘ÀÒü9™E-v´4Ñ™‡ÍóˆÒXì 4:÷Q"¦“÷ ð¼à‰{†}ÄÄV׼ݫ‡’‘@£-DE[ˆÁ°ØÆkA$š?yùìBëaO)+ìôKöJQ®‡"¦“7xv£V,ÅFÆ‘žµ'ÇÛ¯ÿÌC%Y 4ÚB$zbˆÆq0 Èšö5+jyÒGÊäÀŠšCsþ õ0L$Ðtò†oÒòÕX鉻9mü"o¸‡"gE$Ðh ‘@V„‰Á°ØÆkA$P4S Í 7,kúÝÊJ‰F‰šNÞà bYâ›wcýí׿㴟³Wx¨JVD¶ m!7ÆÁ(€H h>¢3² àAÈÞE»•ûb`êåüM#ɺG$Ðh ‘@V„‰Á°ØÆkA$P4ß·" }{¬Ó‘00>;Ûž¡¹#DM'ï'ˆGŒýà‚ø© oß¼åþ ¶] ?Xorpy|_8»øâk·ÞùÀ¦¾ÌûéÌíâßD¶ =$7ÆÁ(€H h~¤è’Œ ì@÷PÁ+Çí8EóAá¨ÒãÑÉ÷É„ô‘²2wœH é䂿žµûœ‰m!( F$†Å6^ "¢ùN¢ø^ŒÞá9 ¿]=îÃ5º­3$gÜ,Î(wr¹¹4DM'O$PH Ñ"¢-DãÆ8 ÍOê(‘@ÓÉ Ý&h´…H h ‘ Ûx-ˆŠæ'u”H é䉊n 4ÚB$P´…H`Ü£"¢ùI%h:y"¢ÛD¶ m!Ãb¯‘@Ñü¤Ž 4<‘@Ñm"F[ˆж Œã`@$P4?©£DM'O$Pt›H Ñ"¢-D‚`Xlãµ (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"qcŒˆŠæ'u”H é䉊n 4ÚB$P´…H ‹m¼DEó“:J$ÐtòDE·‰m!(ÚB$0nŒƒQ‘@Ñü¤Ž 4<‘@Ñm"F[ˆж ‚a±×‚H h~RG‰šNžH è6‘@£-DE[ˆÆq0 (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"A0,¶ñZ ÍOê(‘@ÓÉ Ý&h´…H h ‘À¸1FDEó“:J$ÐtòDE·‰m!(ÚB$†Å6^ "¢ùI%h:y"¢ÛD¶ m!7ÆÁ(€H h~RG‰šNžH è6‘@£-DE[ˆÁ°ØÆkA$P4?©£DM'O$Pt›H Ñ"¢-DãÆ8 ÍOê(‘@ÓÉ Ý&h´…H h ‘ Ûx-ˆŠæ'u”H é䉊n 4ÚB$P´…H`Ü£"¢ùI%h:y"¢ÛD¶ m!Ãb¯‘@Ñü¤Ž 4<‘@Ñm"F[ˆж Œã`@$P4ßIt~ÆŽ Ù9ß­œÑÕÔq+²æe;¹Ê‡4DM'ŸX$˜•U8jŲ>¹“¯œ=iùêÅå>胾"F[ˆŠò ‚a±×‚H h~Ø(:ðÞ¹ïßô|‹-kVÔJÛ•¦üö¯8ðˆÒÿ*|ä˼ŸþÈÜ6 4¼ÏH03kÓkû·ÙzGÓÍ÷­¬¡(Ì^»öªS^ï”’sžØøæ+ç,KÿÓõPŠ h´…H h ‘À¸1FDEó•è •³®Þz[X P:yEÿßzÛ•`%+¢DM'ï,ɨ贮Ç?¶ŸWmW5¡QuËÞZøFœ|PQ‘@£-D¡'V€H ‹m¼DEóEôÇ•s/ÜÖ&jg®IpbÉéÝVú h:yÓH°$½òݵ½›íÌ}/°wõÖuWš›WQ"¦“÷ 0Ðþd ©[Þ®±¼R‘‘@£-D¡'V€H`Ü£"ÖÔ.¯k¨3›-¦¢\±DiqžD‰šNÞC$è²fPØ'kî ´b\Â%™ 4ÚB$zbˆÁ°ØÆkA$ø÷æ'Ìõä‘rnU|™Òâ<‰ 4¼WH€)ËšEz²æŽ{>¸D$Ðh ‘@鑈Æq0 Hq$ÀÛºçËÇ…/ò†+Î}”H éä½B¸ vøˆ½M‡Xàà^IDD¶ „žX"A0,¶ñZ¤8œW|…·ý¶óÜàæÎo•vç2J$Ðtòž Áäåkö«<ÀùSö6eÛ‚—\jˆ|9‘@£-DYU&7ÆÁ( •‘®`¼í±cÍÍóUçDM'ï ø°RU£E˜ôâ!F 4ÚB$ ÃDû]‹TF‚—ò?Òt¿>œºyËýJ»s%h:yON}P MÝsÇ»Tq9‘@£-D¡'V€£~Ûæ•—ÊH€Ý4}¯§.¯ƒm—•¦ç&J$Ðtòî‘ áÃJÐÉ[ ÛºÑùZ"F[ˆ²ª L$H‰ö»Ø”E‚)Ë×úã›HÞ~; h:y÷HðJ~ýÓôá,¼{å›H Ñ"‘ÀocŒòR zæNð¡ZÄ‹ù*MÏM”H éäÝ#ÞУ>PLZžçFIĵD¶ „žXŽÃb¯EÊ"Á;k{ùÐ{G-âžMO+MÏM”H éäÝ#AëmWF} >$ø~åL7J"®%h´…H ôÄ  Œã`²HðÔ†·}è½£qEÑ-JÓs%h:y÷Hpté‰Q¨ >]óƒ%× 4ÚB$zbˆÁ°ØÆk‘²Hð¯ÂG|è½£Ѳäl¥é¹‰ 4¼{$¨YQ+êõ!A»üOÜ(‰¸–H Ñ"Ð+@$0nŒƒQ@Ê"Á? ô¤÷Þ{×ÞðX½rŸ}+kìW¹?ö­;°¢æA׬¨™`uÊëR^~çê—5nXÖ´QÙ¡ð…ÛtgóÃv WE‡—syÑÍJÓs%h:y÷Hp@ÅAîu“Z±å1t;mը܎,o`Š¥3Øç¨^Y£¿uæ°&;ÿ[gŽn^z¬Wž‰m!(‘ Ûx-R å–V±)—]¯4=7Q"¦“wG–§<¾„D?Z3Ð’ˆk‰m!=±DãÆ8¤,àU+!ý¹R(6]Ršž›(‘@ÓÉ»GlV¥<¾„D¬šîFIĵD¶ „žX"A0,¶ñZ¤,|»z\Bús¥Ðò?Pšž›(‘@ÓÉ»G‚›¶Ü§<¾„D'-_íFIĵD¶ „žX"qcŒR ‚à‰勼aJÓs%h:y÷HØ ,üÀܺ*Òiy^Ë’³#=q—DZÝ6&-ˆ‡ëy€H Ñ"¢oDãÆ8 ÍǦعÞeg®\Ž•ìíò?U 2%h:yH€‡ˆY&Æ—šîlþãʹ&”DäI$Ðh ‘@è‰ Ãb¯‘@Ñ|DÇggc‘ bÖ㎢oÿnå {)&Ž 4¼!$°žc—5ß\^'n%Q.¼´èÆYY…&4DΓH Ñ"¬* Œã`@$P4ߊbJ9œÉ»thߨìÐë¿Z”áß~jDM'o  6s2‹žØø¦Ë!¦sŠ/é¿jZXôü ‘@£-DE߈Á°ØÆkA$P4_Ž º¬Ô²ä¬X=Uzüëë¿À¤t97ÂDM'o ¬ç;;sËc_ */þú(>-¿íª¾«&û $¢"F[ˆBO¬‘À¸1FDEóÃF§gåw^ÛóŠ¢[ª88Rß^£r¿ÖÛ®l¿þ³ Ù9a3ñá ‘@ÓÉûƒâ)Ë™ÿô†N§–´ª¶«Z$©WÖèæ-÷Ã-ÆÜÌmâBßD¶ =$Ãb¯‘@Ñü¨Q¼Ž^‘Þ;wâGk¾¿¶oÜŸGä,ž‘U`ÂAqÔÊ( ˆšNÞg$fIzå´åëák¨Ûê1ï¬íõÉšÁýWMŸ½ÜôòQH"F[ˆŠÚ Œã`@$P4?©£DM'Ÿ($¬F 4ÚB$Pô–H ‹m¼DEó“:J$ÐtòDE·‰m!(ÚB$0nŒƒQ‘@Ñü¤Ž 4<‘@Ñm"F[ˆж ‚a±×‚H h~RG‰šNžH è6‘@£-DE[ˆÆq0 (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"A0,¶ñZ ÍOê(‘@ÓÉ Ý&h´…H h ‘À¸1FDEó“:J$ÐtòDE·‰m!(ÚB$†Å6^ "¢ùI%h:y"¢ÛD¶ m!7ÆÁ(€H h~RG‰šNžH è6‘@£-DE[ˆÁ°ØÆkA$P4?©£DM'O$Pt›H Ñ"¢-DãÆ8 ÍOê(‘@ÓÉ Ý&h´…H h ‘ Ûx-ˆŠæ'u”H é䉊n 4ÚB$P´…H`Ü£"¢ùI%h:y"¢ÛD¶ m!Ãb¯‘@Ñü¤Ž 4<‘@Ñm"F[ˆж Œã`@$P4?©£DM'O$Pt›H Ñ"¢-D‚`Xlãµ (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"qcŒˆŠæ'u”H é䉊n 4ÚB$P´…H ‹m¼DEó“:J$ÐtòDE·‰m!(ÚB$0nŒƒQ‘@Ñü¤Ž 4<‘@Ñm"F[ˆж ‚a±×‚H h~RG‰šNžH è6‘@£-DE[ˆÆq0 (šŸÔQ"¦“'(ºM$Ðh ‘@Ñ"A0,¶ñZ ÍOê(‘@ÓÉ Ý&h´…H h ‘À¸1FDEó“:J$ÐtòDE·‰m!(ÚB$†Å6^ "¢ùúè’ôÊiË×[9¿[î˜Îk{v\÷ÍW«Gü°rΤåy‹3Êõ×úp–H é䉊 4ÚB$P´…H`Ü£"¢ùa£³s;¬ûêÂmmö«Ü?mWZØ_µÊêg_øÂ†÷Gæ, ›‰‰šNžH h ‘@£-DE[ˆÁ°ØÆkA$P4_ŽbLཱུ}Zìh–4›ì<lðGæv97ÂDM'O$P4H Ñ"¢-DãÆ8 Í·¢KÓw}¸f@óÒc5v?ꩺå ^Îÿx~ÆŽ°E˜8H$ÐtòDEåˆm!(ÚB$†Å6^ "¢ùˆŽ^‘ÇÈ@$BhP֤ת_í¥˜8B$ÐtòDEåˆm!(ÚB$0nŒƒQ‘@Ñü.k¾? â Hö=¾ãÕvU{nûËÒÿTÊò¦úåNQZœ'Q"¦“'(:F$Ðh ‘@Ñ"qcŒˆWÝ“5÷$ñq%§ÂõÒèÜG‰šNžH (‘@£-DE[ˆÁ°ØÆk‘âH€·uOL|™¼µ®›ÒèÜG‰šNžH (‘@£-DE[ˆÆq0 Hq$8±äô8¬¹'—R^~f‰Òî\F‰šNžH h‘@£-DE[ˆÁ°ØÆk‘ÊH€ý‰<1îqgòIÞ`¥Ý¹Œ 4<‘@Ñ."F[ˆж Œã`ÊHðäÆ·â¶æž\ˆi J»s%h:y"¢]D¶ m!Ãb¯E*#Á±;NöIJÇÉ~xûí€H éä‰J'O$Ðh ‘@Ñ"qcŒR &dçÄmÊ=¼ðó¼¡JÓs%h:y"¢ZD¶ m!Ãb¯EÊ"Á7¹c=´ìqg…픦ç&J$ÐtòDEµˆm!(ÚB$0nŒƒQ@Ê"ÁÛë¾ÛŽ{xá›ÿ£4=7Q"¦“'(ªE$Ðh ‘@Ñ"A0,¶ñZ¤,ügãZö¸³ºtÛ JÓs%h:y"¢ZD¶ m!7ÆÁ( e‘à–¶qÛqù½wí]­²zõÊ}ö­¬±_åþûWx`E̓*®Y^»Vù!uÊëÁÿ@ݲ†õÊ…ýµ.¾Rizn¢DM'O$PT‹H Ñ"¢-D‚`XlãµHY$¸½ðÿÚ;ø(Š´wÈ=39KtDQQ”E×åStñZ]õS¼QWAÑUvU.QD>\D¼r‡#p„#Ü$€‹@îà÷nFzÛJRé™é꩞~òó']3Ý]UO?UõŸî·«žÐŽì¢·ƒê‚‚ë‚ BkÃhIz×À ´\2Óô|I 8<€±€ã ã ðÁXŽ l‹ÏæOzÎóÉ»˜¦çKHÀé䌵€· · ä±…—¶H0åèB=C¶è}8þÓô|I 8<€±€ã ã ðÁXŽ l‹Š=Üë9ÿßóÞfšž/I §“0ÖpÜ$`Ü$cÄ^ Û"Aê¾\z¾¯gÔºÏG‡¾gšž/I §“0ÖpÜ$`Ü$>Ë‘m‘€ ßçÔuB‡ûfO]Ýrûîj¦éù’p:y c- Ç-@Æ-@9Flᥰ3¼œ7³ÙQ[èwdÚI §“0îpÜ$`Ü$>Ë‘‘ qÿaÿ>;˜wè¦Ýù˜p:y ã. Ç-@Æ-@9Flᥰ3ç–z€sòó+»ìØ]ô;“@N'$`Ü$à¸HÀ¸H |0–#›#Áê»iâAÎÀ-î«þ†it¾'œNHÀ HÀq €q @Ž[x)lŽdûÿ=>ZܸßÔ™éîÓâ I 8<€ñ€ã ã ðÁXŽ €÷”´ªnÛÔØ-âsZ á‡û˜gHHÀéäŒÇ€· · ä±…—H@Î_œ“Jëˆý=çÔŸ?eš›QI §“06pÜ$`Ü$>Ë‘ÀíüwŽ|Ak6:‚ûáßò'2mÍÀ$€ÓÉ § 8n0nÈ1b /@u¾ Ó>Zx÷€ÓÉ · 8n0nŒåÈH uþ¤£ó=A  .|qÇ®Zmv†o 8<€ñ€ã ã #¶ðR 畽…f 0öaM\üÁá•LF"’@N'$`,$à¸HÀ¸H |0–# ã|JÒ;*aÔĆWŸ¿?§a.">p:y c9 Ç-@Æ-@9Flá¥0ÎW“+nÿãÉ»}ƒ^§úÓÌê MØp:y ã@ Ç-@Æ-@ჱ ç3ɯnZü@ËêÖú%DÔFÞPzëÜÃß2§2! $àtò@Æ@Ž[€Œ[€rŒØÂK$`œßhr箺¥9ëž(×óôU­ªÚ1¯+Ò„ØêV]˯Qô̇‡¾Ûº»¢Ñ“˜ð!€ÓÉ  8n0nŒåÈHÀ8_O’^HÙwlyöæÏ³7ÒrŠÛvWé9Ê„}€œNHÀ8HÀq €q @Ž[x)€Œó-p:y ãm Ç-@Æ-@ჱ ç[: $àtò@ÆÛ@Ž[€Œ[€rŒØÂK$`œoé$€ÓÉ o 8n0nŒåÈHÀ8ßÒI §“0ÞpÜ$`Ü$cÄ^ ã|K'œNHÀxHÀq €q @ø`,G@Æù–N 8<€ñ6€ã ã #¶ðR ç[: $àtò@ÆÛ@Ž[€Œ[€Âc920ηtHÀé䌷· · ä±…—HÀ8ßÒI §“0ÞpÜ$`Ü$>Ë‘€q¾¥“@N'$`¼ $à¸HÀ¸H Lj-¼@Æù–N 8<€ñ6€ã ã ðÁXŽ €Œó-p:y ãm Ç-@Æ-@9Flá¥0ηtHÀé䌷· · „Ærd$`œoé$€ÓÉ o 8n0nÈ1b /€q¾¥“@N'$`¼ $à¸HÀ¸H |0–# ã|K'œNHÀxHÀq €q @Ž[x)€Œó-p:y ãm Ç-@Æ-@ჱ ç[: $àtò@ÆÛ@Ž[€Œ[€rŒØÂK$`œoé$€ÓÉ o 8n0nŒåÈHÀ8ßÒI §“0ÞpÜ$`Ü$cÄ^ ã|K'œNHÀxHÀq €q @ø`,G@Æù–N 8<€ñ6€ã ã #¶ðR ç[: $àtò@ÆÛ@Ž[€Œ[€Âc920ηtHÀé䌷· · ä±…—HÀ8ßÒI §“0ÞpÜ$`Ü$>Ë‘€q¾¥“@N'$`¼ $à¸HÀ¸H Lj-¼@Æù–N 8<€ñ6€ã ã ðÁXŽ €Œó-p:y ãm Ç-@Æ-@9Flá¥0ηtHÀé䌷· · „Ærd$`œoé$€ÓÉ o 8n0nÈ1b /€q¾¥“@N'$`¼ $à¸HÀ¸H |0–# ã|K'œNHÀxHÀq €q @Ž[x)€Œó-p:y ãm Ç-@Æ-@ჱ ç[: $àtò@ÆÛ@Ž[€Œ[€rŒØÂK$`œoé$€ÓÉ o 8n0nŒåÈHÀ8ßÒI §“0ÞpÜ$`Ü$cÄ^ ã|K'œNHÀxHÀq €q @ø`,G@Æù–N 8<€ñ6€ãújûöF1;'rŒØÂKai$˜7o‹iú 8<€1 €ãú*3³šQÌÎI ðÁXŽ ,³f¥Û¹‘6¬;€ÓÉO™’ØP1;$à¸HÀ4 #¶ðR ç[: $àtò¯¿gé‹kxá· ¿ „Ærd$`œoé$€ÓÉ o 8n0nÈ1b /€q¾¥“@N'$`¼ $à¸HÀ¸H |0–# ã|K'œNHÀxHÀq €q @Ž[x)€Œó-p:y ãm Ç-@Æ-@ჱ ç[: $àtò@ÆÛ@Ž[€Œ[€rŒØÂK$`œoé$€ÓÉ o 8n0nŒåÈHÀ8ßÒI §“0ÞpÜ$`Ü$cÄ^ ã|K'œNHÀxHÀq €q @ø`,G@Æù–N 8<€ñ6€ã ã #¶ðR ç[: $àtò@ÆÛ@Ž[€Œ[€Âc920ηtHÀé䌷· · ä±…—HÀ8ßÒI §“0ÞpÜ$`Ü$>Ë‘€q¾¥“@N'$`¼ $à¸HÀ¸H Lj-¼@Æù–N 8<€ñ6€ã ã ðÁXŽ €Œó-p:y ãm Ç-@Æ-@9Flá¥0ηtHÀé䌷· · „Ærd$`œoé$€ÓÉ o 8n0nÈ1b /€q¾¥“@N'$`¼ $à¸HÀ¸H |0–# ã|K'œNHÀxHÀq €q @Ž[x)€Œó-p:y ãm`ΜdΑü«ÌÌjF1;'Âc92(.>=yr‚äm³©âÍš•nçFÚ°î@‚¦¬BŸ Ø€EEå'Æs.ŠÌ_ ´†È1b›Qн{ -ÚlÚ6KÛ& Á‡¦ÉÜsÊ$`ÜbPÿUXxjÂKR@k ƒ±4ydeå[±Ù ´mÖ$ ÏΜ™Êy¥ý HÀ¸Å$ Ãäæ–Z±{h $f¸6© ™™¹'Zì @ÛfMCräôé)ÒýM HÀ¸Å4$ Ã9Rb9*h $0i$–)›M›ŽZ‹ €Ú6k&œ9óË´i‹0n1 ¨ŸËÉ)?ÞJO€Zà d«Í+ˆ ‡'M²Ì½ ¶Íš‰äȺº3o¾™ÔÔOr ?0n1 È3û÷Yè^@k yðd9­Y“3iR¢„]zÃ" ´mÖd$ ÛÖÔÔM™b «y€Œ[ÌGòÌîÝ…V¡ Ö0@ÉjS‹“œ|Ð÷ €Ú6k>)«ªj¢šœŸ ·ø È3;väY‚ €Zà LƒåË,.nŸüT$жY¿ 9·²²F~«à.cJú È3[·“?l H õ @¾QÚì­^½GòYŒ€Ú6KÛ&ÌKШ ËË«åŸÚw ·ø ÈE?KN’@­a€v}vûpåʬɓå}X $жY?"µ‹ÒÒJÉ÷ ·ø È3ë×’™ €Zà ì6ú7UßåËwHK@m›õ/JJ*d¾W$`Üâw$ Ϥ¤dKK@­a€M ‘6üüóÏ·É0$жY¿#5'äÓHÀ¸E$ Ï$$ì—“ €Zà l8ôsª¼dÉ ©H m³2 YHÚ9íŒ[$AòÌ?ì• €Zà 8ã£=¿Z´h“lT$жYI€ZG^^™„o™ ·Èƒä™o¿Ý%[03@k =Ç}~­çÏO—Š €Ú6+‹$œÓHÀ¸E*$ Ï|ýõN©Â–€Zà øƒ£m¿;wƒýt³ Ý @k G|=uŸ93ÕïÍH m³r"yI’9íŒ[¤EòÌ‚~ï^€Zà ô ‹6ßgúôÿ6[ ¶ÍJ‹ÔLd˜ÓHÀ¸Ef$ Ï|ôÑFÿv/@­a°ùp‡ê7¯À™3¿L›–ìÇf $жYÚ>}ºù«æ¯=ü>§=€q‹äH@Fý׿Òüؽ TÃdfÖddœðW×|-¤@]Ý™7ßLòW³¨m69ùÄÖ­Ç%wNZÚa?ÎH$PÝâÞØ»·RrÃPñÞ{o¿º ùdË–êÄÄÜ“'«ä· J(‰55uS¦øgi$ AVÍ[¸{w±$fh¶kÖäø‹ €*¬][š–VP[{¦Ùë%Ãï¼³Æ/T`s$HO¯HIÉ+/¯‘Á(ƒµ¨ªªõK›µ3ìÜy&>¾ ;»ÔZV¡Ò&&ð   RRŠ7m*¤G~Öú›1ÃÁ̶E‚´´Sk׿WW×YË$(­T TVÖ˜ßÏÛ ¶o¯Ï;vì”Tð¨0?þ¸Ï|·Ø ïØQäÑe’ggb˜·Þ2;lɆH°fÍÉ êÀòXßÊ%)/¯ž4)ÞÌÛvC òIHÈ-*ª°²M~-ûêÕ»MžÓÞžH•u&!¡`ß¾«{†Â–¦M35lÉVH”d`$«{؆å/+«š81Á4*°lÚT™””[VVH¦Z±"ËÌ9íí†;vÔÅÇç>\0ž¡°%3ƒ™í€î`¤]»ð6AÀ´é*RRRaÚluv@‚ ÊSSó**j¥»ÒFhÙ2óæ´·ÐÝ$z´”Ÿ_nÄ%’ëô€Û´_î`¤ƒ­Œ$—#Q œ8Qn6Ô‡…ç[%,\‡/ßå³ÏLšÓÞH@¯ŒÑ£¥’’@~eÌ´°¥@E‚Fj¼+Á§+PXxjÂáqŠž‘a½°p¯ý¸dÉ~ú6Ð+cÉÉy§OÛâ•1s– ꃑòŽ„`$¯{è/òòÊ&LˆÚÕ$&mßnÕ°p_œ¶p¡ð9í ìùʘ aK„›7WQ0Rii@#ùÒáàX¿(ðóÏ'…Þ+$ °ðøøÂ½{-î‹ÍæÍK ‡g_³Ú,¾¸Dslii¥Ð”Œ¤±6­¡@NNñøñ¢ž PXx\\þ¡Cî‹/çÎ8§} !^sی–Äýè°:¬[W––øÁH¾t88Ö/ ìß_$¨ÙZ ¶m ذp_l6{özA÷   ³²ðÊØ-&.lɺH`·`¤ÿº[Q`÷îB·ø,ŠõaáyÅÅXtÆ/þš5k­*°4œ}eì¤_®ˆä™ [²"Ø6Ir‹¢x ر#Ïð{–C‚Œ ϵIXxCèÿdæLãç´·(¸_;zÔÂóWë¿î^ï)"lÉBH@3!Ékóà@)™™kì܆B _³&¯ª ³‡ërÍi?}zб÷ ,‡îWÆ ñʘ.Ï:TlìK ‚‘t™;ɪ@FÆÏ®wc $ °ð h%›†…{íD¢‚iÓŒ\éÆBH@¯ŒÑ"õxeÌSó8@aK†M¨.9¸ƒ‘òòN{ªö‡R)–vØ(* ’“OlÙR(•øÖ* q”sÚ[ 6n,§Eêuþjì·w¯aaKÒ"ÁÖ­4G%‚‘Lp²0I5kr ¡i‘ !aáÆx‰æ´Ÿ2%Ñ'’#½2¶n]>­ìcŒp6>‹QaK"AFF%‚‘llí@®zbâß©@6$¨ Ï?paáFZ·ªª6°‘ 5µ$=½€”àÏ( [’ 6l8`$£ìóÈ©À?îó‘ äA‚ú°ðüŸFX¸¯UTÔLžìë3b ï$%mÛv\ˆd¶?)…-ùÌ,  Éö^¶‘«Wïö¥«— ¶m«¥çz íÚS§ª}ìáåA÷"õ{ö‹Íæç÷1lÉïH€`$›ØžÕ_±"kòd/û êÃÂóNž ä…h¥ò¤sÚË€îGK‡a‘z“œåKØ’‘ !áøÎ˜£Ò$“ ÙX¶l»wTà/$ظ±aá~qQII…×Ó`ú ènR||^n.^3Û8^‡-™õËŸìß`$³M‚üdSà³Ï2½!3 Ö¯GX¸Ÿ½STTîø ðʘŸóË/Þ…-™‰4ãP|<‚‘üî@"–,Ùâ)˜‰î°ð:¼"&e Ny1OùHPÿÊXEAH ™Ý‹àEØ’9Hà¾}„`$»õoLE‹6yDæ -D»m[QcåÅg~S 7·ÔS*0 ÒÒþóÊXee­ßBÆ ð4lI4 ©Á%ÂP€U`þütýT  Ü+‰ìÞ°pöI’>r¤Ä#*0 Ö®-¥Eê1µ$&aŠAaKú»qHàF*/¯aЇ$€ ˜;7Mg³„î°ðìl„…7¼8r}’}B?ˆF‚””›6bÆ!¹,Ò 4úÖD Áúõ§0Geƒk‚ @3 ¼ÿþ:=T`8 ,¼™ #ß×ûö×Iâ 1¯ŒÉ猦K¤3lÉX$@0RÓß@æ˜5km³T` lÝZ“{âDeó%Ã’)°kW*0 蕱„ze¬D2=PœæX¸0£ÙîÅ($ `¤ÌLÌQÙüEÁP€¯ÀÌ™©üfklÚT™”„°pþ¥ý[=+݈îEê)“]”¯iæÍk&lÉG$¨Ÿ£ò8‚‘š¾ø x¬ÀŒ)*ð h%‘ÔT„…{|Qä<`ëÖcü A‚ÌLº›”WPP.§(•G ̙à [ò ().®À„`¤ÂÂÂ'{,;;Û£Zcg(`](Xkڴ䦨Àk$¨ /¨­ÅâsÖµF#%§•n8 iùˆ[¶T'&æbþêFt·òG³g¯oª{ñ hù³¸8óæ¨LHHèÞ*"âÁ{ïݳg•¯Êô*@ïs½ùfR£ÍÖ $HI)FX¸^é-¸ßúõ‡š¢¯‘ =ý?óWã•1 ÚAW‘ß}·ñ°% þöQnQQ…®, ÚiæÌ™£ÃÂJebpp»ÈÈûn»mÛ¶m§ò*P]]7eJ#K#y„¾cf’÷*U²””ìF©À $HK;µvm>ÙϨ²áV”_êÿ;¥(oµhÑ>2rèM7¥§§Ë©3JŒR ªª¶á=H€°p£.…Γ°¿!x„´HýƘ¿ÚBÝ—¢ÒÊéÓÙ°¥f‘ÀŒTQá·9*ûtî¼é,¸Á BQÞ ºÀá|íµ©©©¾h‚c¡€ä TTÔLžœ >¸WAX¸ä—UPñ®t£ h‘ú­[ñʘ Ë"ïiëÖ~ó€’ƒŒ´~}¾ƒ‘jkk]aaÄnÐþ¿ZQæ]ätìÓç§Ÿ~’Wt” ø¦­ £]¯)$ çz´m~>ÂÂ}“ÛâGûín-Cò‘ þ•±B¼2fñkîSñkjèå© Q$'içÎÝ££µ$Àl×*ÊEéár]Ó­Û7ß|ã“48 Ȫ@ii¥J ‘€ÂÂiÆ¡’’*Y‹r™ªÀŠ;'Oþ5 ¥)$¨Ÿ¿ºààAÌ_mꥑ33ŠQïC2H”T´}»DÁHK—.ær1Ð0yFQ–)J_—ëŠÎ¿øâ‹:< “Óy(• ””T¸©@‹žœœwú4VñAÙ@<ôË/]é¦!Ð+ct7騱SXoÔÉK(lÉ‘n$ `¤øø‚}û¤›£òïcƼÔšúdµ¢\ëruëÔé“E‹è¡ƒ—êà0( ¥EEåDn$@X¸”—H¢B¹WºÑ"û•±ãÇM}eL"EP®¶Dá©›7WÆÇç>,é•·öïÿmcM!ûóE¹ÉåêҮ݇sçVUán*×øÒR œZ°`[}X8f²Ô•óGa/ÞâFZ¤>))·´Ô¯Œù£ÞÈÓK(l)?ÿ´—›rXÇØØŸ=G7¬W”!.×ùçœ3ëwÊËseÊC&P ȤÀ²eY4µ_“I ”ÅÚ ο!Ðì·›ån§³CLÌ´)SÊÊ$½bíë„ÒC( ¬Meüû˜˜f}=;d)ʇ£]TÔ„þ³¸¸XpÁqz( €PÀHh*ãçÂÂôŒø:÷Ù¯(FF¶q:_yáZMÉȲâ\P @( €0h*ãùÞp á°¢<ÑÚáýä“ÇŽV|œ @( 0F+»tÉ€nZÈS”±aa­##Ÿ|ðÁœœcJŒ³@( €F+À™Ê˜sÀÓ¯ŠåÕÐÐ6‘‘ÝwßÞ½{®Î €P øª@VVÖeQQžñÞíRQÞ iïp ûÓŸ¶oßîkÑq<€P @(`œ4•ñ_tLeì4zÔiEy»E‹NNç]7ßœ‘‘a\Up&( €PÀ{^~þù7<™Ê¸ÑQÞ‹+eNPÐ…Nç-¬Y³Æû àH( €PÀh*ãUÂb ›E…EYt‰Ëõû¾}ãâ⌨Π€P x£MeLo 6;v Ý¡NQ>S”žQQý»w_µj•7ÕÀ1P @( € ÐTÆ-}žÊØZ Û ÅrÓõ×ïٳLJ:áP( €PÀch*ãMeì5 |¬(97]uUjjªÇuÀP @( €Ï ÐTÆÏ:•±G` ÂÀýú¥¤¤ø\œ @( ðRšÊxž? Ttå•ÉÉÉ^–‡A( €)@S§›‹* Ð+€ƒ.#N €PÀ'ÜS—›…nèâtìÛ7))ɧ¢ã`( €PÀ8h*㮦Le¬ÂÀ }ú$&&Wœ @( 0@šÊø>ÁSÿ Á½Ý`@¡q ( €PÀhh*ãɦ2VaàúÞ½ããã.;Π€P ¦Àk¯ýF@ zõÂņ]-œ @( €0:µlyH(J¿ˆˆ®¹¦¦¦FXÙqb( €PÀ„Ne|JQns8† xêÔ)cŠ‹³@( €b Èÿ¢£=šlУkåшˆ¾—^š——'¦8+€P @(`€4•ñ3>Le|T߇‰!!¶m»{÷nJŒS@( €ùç?¤oXgnü¬(OGD„·hA1ÌW&? j…õŒ\Cœ @( € ÐTÆôéê@ï†6N縱c»_pAªîÃT”¶ÇŸn@¹q ( €PÀ8ÜSŸÖ=¦Ócº3@0ðÊ /P\"äþ;îðh½¤­ŠÒÉáx{Ú4ã*aí39s&33ÓÚu@é¡€PÀú ÐTÆ—è›·`ào‘‘4¯ÑñãÇÕªO˜0á…ÐPõ‚ž z᱇ÓùìOÔÕÕ©ç±ÛÆ®]»Þÿý»o¾¹µÓ²|ùr»)€úB( €T ÐTÆ÷6‡* ü}Ì- ¸+òùçŸÿÉóŠe Ãq÷-·TTTH%ˆÐÂdggÏŸ?ÄСçÅÆ^ètŽŒŒ\¬($ïwŠÒµcGÌÞ T|œ @(ø ÐOþIMOeì†VÇK£G6zªmÛ¶]âÕ’I•Š2,2òÚ+®hˆfdÑsss—,YòȰa¶iÓ>2r˜ÃAÁœ<©ùƒËõ¯>°hQl( h*ã †'ºùLQžŒ$xñ¹ç 85¥ŸùaÁÁÕ¤Ù‡gelXØ¥;ˇUúý»ë·'i´;̦Ñ0==]=¡7ÊÊʾýöÛçŸy¦wçÎÑaa·DEM ¢¾N·2÷;¯gź£ÌP @(`uèÇlŒf’"‚çêï  äççë¯Ý­×]÷•îO êörEiíp¬ZµJ¦2ìIwHhmÇq/½Ô¿[7WXØ èè‰-Z¬Uïî™›ÑmÌñ(ÕE €vS@ÊX…1Ï<ã ¸{î©§Þô ˆ Ö)J»ÈÈ9Ò?O§ ÀuëÖM?þÆ>}úGG¿¯(>+@"<þäCÙ͇¨/€P ø]šÊøžÐP÷ÑO?íõïÓ9sæ<äp¨?ù½ÞØ£(ŽWÆŽõ»2LèeÉÍ›7Oë­!×]Ñ;*jLhè*E)5´rP”6‘‘{öìa €$€P @¡ Œ><’à©§(*Þ—Œènõ11Ú¡Íëí|ZOÙéqÏ=ÕÕÕ¾ÉciÒ†Ù³gßõ‡?œãptu¹ž ÿRQŽŒVoß5x°!åÇI €P èT€Þ‘÷ÜÑI(®žÚ¼Nº×S¾ñškJJJtVÄÀÝè݇yóæ ¿ýös££iꀇ#"ÜSx]O¤w3çtÒ³ +…SA( 0M興Bã~>ÓzÊEDôìÒåÈ‘#&TáèÑ£‹/yß}¿kÝš¦îpÐÍ«Ž§T°8(h@¯^&TY@( 0\«/½TÿâG:‡ÈI!!Zµ¢©|/-­#@/TÒ©~øá‡E‹M:uô¨QÆ ù}¯^Û¶¥[wé˜:@g±}ߦkèõõ×_û^qœ @( €É M\dÁ¿Ùºÿ§Ö-[bŠãf½‡ €P@*vìØqÃQ&lh¦ÇëÍŽ¡´C‰¢$гƒÐÐ ¯½&Õ…Fa €P ð 9ý1¢‹Ó¹Æfc·Qâf€·å/NçÅ.Wtxø —_>úÉ'¿úê+¾òø @( €„ |óÍ7çÅÆ¾fÅg÷F î:ÏsRQ…¢Ý UÏÏ=ñ½¹k×.š/QÂë‹"A( Я@aaáƒ÷v¹¶ãvÁoP`¸ËE÷ˆôèA ðé§ŸÒ4‰`ýÞP @(`!~üq;—kzHˆ„ï÷éüïûnÄIõ÷ˆ.Šr3À³?þÉ'Ÿ€,df @(|T ''gà•WþÞå¢5}^-qZ‰`†¢Ü_Ï´NâuÝ»ÿí±Çˆvî܉û>: ‡C( €u ApúÔ©mŽ…AA–Ó=-$1@òYèåf€g}”&H¤W00Ñu­‹’C( €¶oßÞëâ‹ït: ¬»ÀÍo×ßp3@ÿn݈.\HÕˆðÎ  €¤@UUÕßÇŒéàp¬´ÐL t€€¦A&p†…<ýÈ#4ƒ" ,Šº@( €™ ¬Y³¦ó¹ç>)nF#Oïü7ÜŸÊ–¢(35 pÍe—zøabZ5 ÷Ì4Œˆ¼èNâÙ¿C‡y‘Í'yö‰©©©^œ‡€Z¨~Ðn7233>•E € ÊÊÊ1¢³Óiø²‰ wŸ¨ ð׳÷®îÚ•`Á‚Ô¢i &ARà´~Q`Æ ÿüõoèС^”aÎZ=ã׿Ûn»Í‹3àP€Ö[9ë‚fþýãÿHý õ~PqTA„ôcóÈ‘#›7oþñÇ¿ûî»õë×ïÛ·ð‘—„ç\¹re‡ØØ—ÂÃM›Ñ¨VQ( _Q²%«þYÝ èí u3ÀüùóÁºÅØ"ÑÙ¡¡¡îN<,,¬¨¨ÈÓó_ýõê°|ùrOÇþ¡€~$PÝÒ¾}ûeË–FõQ C(..¦u{é·Itt´êu£eË–·Þz+Ý ¦u~ ÉNæ“PÏL3õr¹¶5]P­(ôRž¢TZÕ(½þfþ÷Šò•¢üŸ¢Ð*Ìï)ÊTEôRHÈ38ã¢îŒ‰ùŸØØbc¯Œ‰é}¡ËÕÞሠmÑ"¸E š ËuA«V];t x€Q#GP»Æ}™}"¢lwÞy§Úèè'¿GYЫµAAAîÃÛ´iS]]íÑáØ9`ð ܶ™5kVÀˆ€Šx­ò´ž/ új_ÄÙˆ‰‰yå•Wìp—éã Úº\}cbè×úï\®v‘‘Qaa!-Z„ÇDDœKÃz›6=:uêwñÅ7ôì9øê«ï4høí·?ü—¿P€ß‹cƼþÚkS§N¥&öÑG-Y²„~²Ñ-—äää7Ò#ã;v¬¤¤„‚½¾p80ð ›Tj룟üUZ±zìèÑ£=:;’Z$8p`êQN3’ÑbîÆ kÑ¢…jBJê£îOì£ÀÁƒ»uë¦ZB»AV‰ŒŒÔ~¢nÓ]&šß&àU¢Q{Ó¦MÔvè÷W~~>µ#„ðüE÷oé¾Ð¹çžënhÔ?{dØ£Gµ…Ò+'þ­r÷£Í"¶lôŒØår©ÎéÝ»·ö[lÛJ 6Ð FÕ ´qÑE½úê«~üøq÷Œv4R,Á¼yóî¸ãŽððpuçÕ«WÛJ+T ˜£À‹/¾¨¶2úá¯3S 5QºòÊ+u…ÝR€ Ç”ªyhƒ:ü€”•â+——§þ!Ð<÷~ø!ÿá5ýfy衇‚ƒƒi _^| ¼S€ÖšTûgúá¯ó$Zøàƒt…ÝRO‘€~ýEDD¨®£) *ÅQ€<0hÐ ÕmÛ¶¥;äœýµ_Ñ;­Ä@­&؆*пµmöë×ïj„ôîC¨o§Pa ƒSYNO‘€*سgOÕrï¼óŽåªŒû¨ÀÒ¥KUÐ#ˤ¤$NHÙ÷îÝëÑ!Ø @ PHªÚ<=Ý>|¸Î\°[ *àÜ|óͪÓè9B *ƒz5¥@Ÿ>}T<õÔSMí†Ï¡0_ÒÒR‡Ã¡¶P6~úé'ó Œ¥RÀ $èܹ³j3Š%“ª:(ŒhRRhZÜ_ÿ(0À£¨fÑeÃù¡ xà³mT¡É ^kúo̘1êž\p}fƒOšRÀS$8yò¤ú*bHHÅ–7uf| üãÿPûÛo¿= ëˆJAK+@ÏòÔFú׿þ•SšVÝ“À³'¾²‰ž"Á£>ªZhÔ¨Q6Q ÕTÐÎzŠHUl@y û]ºtqwÔN§“39ÍEãÞ‚‚²³³å©Jâ/ô#=¢¢·ÎU ™èhùúEêj´Ó ¬[·Î/Å@¦P ð˜8q¢ÚW/\¸°Ñ‰ˆÜ»Ýxãîƒí¦€ ºwïþUƒ¿O>ùd„ ôFù9眣z¬C‡4m‘Ý´B}é%Õ´ÇF°SZ»V}ÂÛÔp?~üxµ9/^¼XΊ T&+ EÕœ ú‘xï½÷b­d“/“$ÙÑÏ ­7øsIRf ØSÁƒ»[+±A£=6M7êÞnù–——ÛS%ÔšQÀ#$¸ä’Kp¯˜ÐVÉ-[¶¨H@Kتî¨,°–Ÿ}ö™ÚZßxã ¦ðô²˜úíã?Î|‹¤mÐ"Ý ÷P´:uÒ>;v[hÀ€¹¹¹¶UÌΧeøÔn„Þ7±³¨;\ÊÊJõio×®]™ÒjÅi½æ[$m«€ ] ‘"Ê(Œ€“–ÃP‡ƒŽ;ÚV4ÛVüèÑ£ªhƒVF¶­¨8_§Ÿ~Zm°´²¶ZàŠŠ zXàþŠBÈÔϱšEU"bƒ!C†¨{饗ԯ°aè£jÚhô¥M¤@5¡€ü Ðâ#jƒ%' T´HpÙe—-ýíß’%Khµ;zß𪫮RD´X¦1 TKè©×Œ3´~xþùçéÕf,))yì±Çè@úÙÒìÎØ @£xùå—µ –¶¯¹æ£NŽó’Z$`<ÓT’Ì›7/D@]¼P€B•´éׯ_BBBSç¡gLS§NmÙ²¥û ASBás( B½{÷j[+mÏ;WDF8§Õð úöí»víZ«×å÷]š.uذaL?sþùçÓÃJ i¦õXÿýï¿÷Þ{ãÆ£°Õ—ž€¾+3@/ ‰æ(žÐýGíôäÉ“^œ‡¼;wî> stream x+TT(T0P04¶Ð3W0·4R066Ñ3Q(JUWÈSÐH-JN-()MÌQ(Ê«Ô)0VЩ5ë017Ö3äJÎUÐ÷Ì5VpÉ·`, endstream endobj 32 0 obj 85 endobj 30 0 obj << /Type /Page /Parent 3 0 R /Resources 33 0 R /Contents 31 0 R /MediaBox [0 0 792 612] >> endobj 33 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im3 34 0 R >> >> endobj 34 0 obj << /Length 35 0 R /Type /XObject /Subtype /Image /Width 586 /Height 1388 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xì½Ï«-KvßyïD˜óq†F ž5xàž4Î@¨=¢ £‹% dÕ@$,ôÊô u!p7mÊ·„®Õ*=A]ÅEv‰ªzuJ÷=qŸn"¿{Ç“±2öŽ\ùcï8$ûDFDFF|WÄúF¬ø‘>ô¿Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðèt:Ž@G #Ðè¬À7¿ù‰Óõ¿ñŸ~ú)‰÷ߎ@G #ÐèŒàÖ•ä~ï÷þÝÿ÷æqÁn®9ï‰w:ŽÀAøê«¯:»Tv=ÛŽ@G #PB ³[ ™îßèt:ÇE ³Ûqe×sÞèt:%:»•éþŽ@G #p\:»Wv=çŽ@G #PB ³[ ™îßèt:ÇE ³Ûqe×sÞèt:%:»•éþŽ@G #p\:»Wv=çŽ@G #PB ³[ ™îßèt:ÇE ³Ûqe×sÞèt:%:»•éþŽ@G #p\:»Wv=çŽ@G #PB ³[ ™îßèt:ÇE ³Ûqe×sÞèt:%öÀnùßùÿäÿÉþâi|®ß­$ÖîßètîÍÙo›þÓŸ{a\Ýöâw:+؜ݾùÍO^¼xùñzx|Á…Ý®k¤#ÐèÜ9›³c·HmÿÓ/ü¯_ÿåö»¿ý[ôŸþ3×üîã·³ÛWÑ^üŽ@G #p›³üõ¿ò'ÿø_}óūקQÛ‹—*_ÿÃô«?ÿßÿ•ÎnWˆµ?ÒètîÍÙ-’—FjßøV`:qq@#L}UÉ×Þ^üŽ@G #PB`'ì¦5“ùí¯ýŸ¿þ¿ýæ?y©ÑܔԢOg·’X»G #иs6g·O?ý $vH¬‘'R{xü¹_üCFpX,û¼Û×Ï^üŽ@G #p›³ÛhUɯÿú¿a®‹qœ®8R›:úØí:¡÷§:ŽÀÍ#°+vûhLöLI-útv»ùúÙ Øèt®C`svÃ2VK–¯ÈeSGg·ë„ÞŸêt:7Àæìg±Z’_–ÈÌ5e´ÔçÆØí¯ÿÇ_ÿûÿµ_Ž@G`qP/7Og£îÝ"aiÁ$ìÆ²IÍ…O :n‰Ý¨ÌÿüŸÿ/ýêt:N|þ“ôÿmßî‡Ý8¥Dón0Z:§1Ý”Úð¹=v£P‹÷Ùz‚ŽÀ#€…1ïmø¶vÓŽvh+÷¿þßGМ±)àöØíÞªßmw{é:;Arïìæ! ÖügG^#O×ð„ݸp„ÑÜÃã]Ý:»yÔÀžfGàÎèìæT*ÙMC6vvCsŒãä ¿1"Áô¶ÝœDÖ“ítn ÎnNÒ¬d·ñ/9n‹î”ÎFîÎnN"ëÉv:·„@g7'iV²»8}+½ôÕ€£¥·ÝœDÖ“ítn ÎnNÒ¬d7h‹)6¶¹Åc¸¸5–”¿³›“Èz²ŽÀ-!ÐÙÍIšõìÆ\Ûdz”_¼äheN2Ik#wg7'‘õd;[B ³›“4+Ùíã6·‡ÇøSnÄhémg7'‘õd;[B ³›“4+ÙM£6æÝ°FrñS-,1Œ“ÝœDÖ“ítn ÎnNÒ¬d7qY:4뻹$Ò“ítî ÎnNâ®d7íwéíÛÚïåõÝÜNréÉv:w‚@g7'AW²Û¯}ã ß´’Dn •éhnäî–I'‘õd;[B ³›“4+Ù æâÉŸûÅ?ÔDnGt6ºíìæ$²žlG #pKtvs’¦ÍnµÁ­ô;b´ô¶³›“Èn YNì¤E÷k·ô#U×leݜжÙ-|›ûÅKãJéläîìæ$²£'«¶Ìª¤~í{ûÜØ†Íª³›ø6»Vþ³Ç-rœö»GŒ–ÞvvsÙÑ“U[¦ªàè×ÀbíöáÛj M-âÞßÃ÷Ý´ÁÚÁ1×¦Ûø›ÒÙÈÝÙmµÖq¬Ýg[>Œº€VÖ}¾vgiGÀˆ¿ìÛÎn+·‘£¼î>ÛòQ¤C>»€VÖ}¾vûGÿà»ýæ?yÁ‘\lvÓ…Û ¸În+·‘£¼î>ÛòQ¤C>»€VÖ}¾vckÛÇ©·8÷âeg·• ¼î>Ûò×´²°îðý°,–~Ü-º;»­Ünàu÷Ù–$¸. •…uŸ€ïŠÝ ²xîVttv[¹!ÜÀëî³-Hp]@+ ë>ߜݾùÍO8[òë¿üϸp0ï6úíì¶rC¸×Ýg[>຀VÖ}¾9»±2„é¶ñ/Y;¦SoÝVn7ðºûlË\ÐʺOÀ7g7öµ±Ç=Ý\8¸¾þÉÈ!ÿÎn+7„xÝ}¶å ® heaÝ'à›³[J^šk‹3nÑ‘ÆIÝ}GÀÊmä(¯»Ï¶|éÏ. •…uŸ€ï‡Ýú¼[?›¨±ÉÓ„•] æÏ~öto5¸Âã÷©lW¶ôŠû|?즳Jú¼[©~v”ÑõÃnðœÜ_¾·ï¡k"pŸÊvM„GïºOÀ÷Ãn¿þëÿjc›[œtëón£*Úo ¨É¬Mb .qÄtb7l·dÁ68PÐ}*Û tŸ€ï‡Ýú9“ј¶a+8ô«¡6Ž*ÙMšÛC—ëö2ŸÊvC9Þ'àûa7Ù$9XSÒ_~çwâ/ª©tÝRŸü>«ß²íù5zª-²LÂwŒÝ~ö÷˾§§ÖŠ@¯í­^øü}¾v‹ónÿôç^¤W‰Úðïìva¿ýè²Cj®íó/~m•·_òC•ð>•í†"ºOÀ÷Ãnl|{¡/–r„²>f:ÜvvÛ°QîÕâµô·/˜Ü¡ïSÙn(ˆû|?ì‹¡”à¸O?ý4~ºGg· Åá^ÍJl’¬PÒ/C¹Ãá2|ŸÊvCÉÞ'à»b7&ÝR›$gNÔFP·LnØ^vûjjÔ¦‹þÒnóyÏ»Oe»¡Äïðý°Ûw~ÿW¦›Ýè{×ÙmÃö²ÛWSa"»iSÀn³z·»Oe»¡¸ïðý°£6íwcy ›ÝDvt¿K×ÙmÃö²ÛWS¥u(÷-UÝ¢}]ÆîSÙ^‡Õ"OÝ'àûa7qD&vSOcêí–Ô×}V¿EZî460|£ MƒºÏèµ}e)Ü'à;b·W¯á2¦ÞÄnlyßuv[¹!ÜÀëX'ÉðíÇpWÿÛ!÷©l7Ä}¾vûµo|":ã—Ž·æ‚éJfIüûØmÃö²óW÷õ${Ð}*Û %rŸ€ï‡Ý`+™Ô–7H-Ý«×ÆÀ­³Û†Åx5VA§‹»EHü@¿dÕÀênƒîSÙn(îû|WìaýÇïþ7 ·öÕÇn¶—Ò«ï÷þ-µ«Ca·ÒK÷é¿BãÚgÁgsuŸÊv¿÷ ø ÐOÝuvókW§ì'îÎnW eoÞ§²ÝP ÷ xg· «\úꛩ~Ý¢XWh\ñ]ÇrÜLm? ì÷ ø ÐOÝõ±Û—Ÿ¸ûØm‡â¾.K÷©l¯Ãj‘§îðý°[ºk;uÓ4Ý©ùË&ÒÙ-â¹BãŠï:–ã>•í†2ºOÀWh€¶ºcQÙëøBKNÝ6lW¼Ú·!ÍÙ >v»Bû|ä>•톲¸OÀ7g7xÇmnY‡¡ôúØmÃöRzõ^ØíýÛ\›þ­Ð¸6-ßõ/¿Oe{=^ÍOÞ'à+4@[ÝØíÕk6».‘]g·æª½j¶¸ iÎ]4vãüíßýíßZµä“—­Ð¸&ï<†Ç}*Û esŸ€¯Ðmu'vÓ©€©~cê­³Û†ÍáêWÛâNE|©û"v#rg·«…èýà}*[oTôïðÎnF•X3èfªß¶ìö³}ï»ÿÇÿÌÅ''˜Ã•ûó/~¼¦(ã»Vh\ñ]ÇrÜLm? ì÷ ø ÐVw¬*aŒ†.­“ä8å>v;JÛIói‹ûÒñZ¿fìöæéïîÿðçó»ã&¿ªœæp5÷ kµ²,û¢ûT¶ËbxQj÷ ø ÐVwº%£§ýG¿úó|Ã^cñ¤<9é=Õo#w_UrQ _'²-î‘/º­a·XÆn™ŒPìÐqŸÊvCAÜ'à›³ú-žüâeæzõÚ>m²³Û†í¥ôê°ý¢Í«Ç «$…ûß§²ÝP(÷ ø °FÝñ%n,HX“Â7ø–PÚÔ-n®¾¬®7SýjÄ}Ñ-F¾hìö}ÿVÊtER+4®+rµ‡Gn¦¶ïÌš<Ü'à+4ÀzuÇÔ›>yçK1Qþå·¿5[ÖÑÙ­¦b¯§^ÜY™ž±Ûʥξn…Æ•}ïþ=ïSÙn(—û|…X©î`4¦ÞX^¿Ê­’ 8CÝuvÛ°½”^])nC¬¥ În%ÌçŸÊvC1Ý'àûa7¶¼…©·‡Göáʬ”/^ŽÖR¦ª¯³Û†í¥ôêÎn™W|×±÷©l7”Ñ}¾B¬TwÑ^¼Ä2ɇ¹q0éÆÂ€În¶ˆë^])î´—Réîc·ë$²Ã§îSÙn(ˆû|oìößÿìßÃhŒã²a¨dy‰¡úúØmÃöRzug·ˆÌ +¾ëXŽûT¶Êè>_¡Vª; Ù˜wƒÝ¸µ 4×ÙmÃFqÅ«+ÅmˆµÔÇnWˆcŸÜ§²ÝP÷ ø~ØíëŸüxmô[Òuø÷±Û†í¥ôêÛf7ÚK©àSÿ×ô¥‡ð¹Oe»¡hîð`¥ºÃ Á±ñ+:Ø×ÙmÃFqÅ«+ÅmˆµT?v{÷Ý;Íùßýà[œB9õ|ž>¼{ÃõôaæDÊ?þ¥$B«ùꋟþìgO¬é-$¼Wh\ÆÛ÷tŸÊvC‰Ü'à+4ÀJuÇ\Ûè´Icµ¤`»mØ^J¯®w‰Â ÿzvcøÏ©n:B9þâIÊdûÝmE¡úáøêGš‰sö"â@jTWÏÏ!™ÿ+4®Ì[àuŸÊvCÉÜ'à+4ÀYuÇ©—¨:¸ÕE¡î:»mØ^J¯ž·!P;¨žÝ4‡ËÀ?€Ã4î«×á\å»’zõš³M¸ˆÏm©h4ù×ÿáûœ‚ÂE|6­”"ã¿Bã2Þ¾ç ûT¶Jä>_¡Ϊ;颠…r—1‚ëì¶a{)½zVÜ6…¡õìÆØ û¶rˆCöCªÙt솑ZO)2_à“c¶tØ- 匜?¹¦ ¦®Ð¸Ò×È}ŸÊvCÝ'à+4ÀYu÷oý ýgT‡æÝø>Ý6lW¼zVÜÙAõìFE¢9+ó",ÜŒ¹"‹ÅrýìË÷DþðågòaNÛ»'¬é=…ªÓbRSÇ kúÒCøÜ§²ÝP4÷ ø °RÝAgZÔoœÇ¥µ%Ñgêèc· ÛKéÕ•âžJsÖçv{xdÞíÆ`OáC¦ý:cp§é6ž2"k ‡¢ ÕðÇPÆ,á€?qø|¡ánƒîSÙn(îû|…X¯îôe7kh:É\øJ¯³Û†í¥ôêzq’ÍÕ³Ý@RªB×7þËOK¹¥þ£Á«×t®ˆÃSFäŸý}ˆ@²q^xàÐRÚÝŠÈܧ²-ÂápŸ€ï‡ÝNÇK¾x:ÛÉ\·Lú×ý%ß°v£<¬ú•{j•ÎÒ…ÿ÷ÿn8¾%YèO7c Ç“+4®qþrŸÊvCáÜ'à+4ÀJuG—RC#i‘ ªCç*ë›8ÙÎ|»mØ^J¯®wV ¶gýØ-Σ‘ỈÙíoiþY%¢8Rí…%g…Æe¼}ÏA÷©l7”È}¾B¬Tw¯¡ß`7.a4÷ðØÇn6Š+^])n›È²¡õìFOiظæÝ´PÄ(Èé‹Kƒ}’JÈ­ùÛ_û›ïýß,›Tuí–I+#è>•­ˆwÐ}¾vÓ-Lë¿x‰v’ƒá[VÑɳݼÅéïݨBLÝÆÙ1n‚PÙ°È8‰#0ca@Fc!)Œ ÄÁ8f÷Ì–+4.£\{ºOe»¡Dîð`¥ºÓ÷nP£«³Û†âŠWWŠÛk)¨~ì¦3K1nÃAav¬Ìn,þ õå{•T>ÿIþ<.ü‰ü7Ÿÿmx„Ù:m8?;Åj…Æ5}é!|îSÙn(šû|…X©îè ÿãõÍô ‹ÙKºÿ>vÛ°½”^])nC¬¥ zvcÔÆ(L9%•r+:‹§oýä.þÊÆê#M<‚yêš)Ï—ñö=ݧ²ÝP"÷ ø ðjußKJ:»mØXŒW_-î©EÿJv1;¦L2Ôâ|-#Ã:}Kg@‹vdèŒ×?ú¿À/öO#å—ñö=ݧ²ÝP"÷ ø ÐVwLâÓž^ì gTnSG»mØ^J¯¶Å=b½O%»Q¥áí_S&Kóh eŒ&ÎÂao )ÖIêêçL–êÀ¬ÿ}*ÛYXü"Ü'à›³Ê[qÚ¯³›_s¸:åÍÙœ†zÿãaXáÿþílYh"¬8W|ä|f—"|öÿ*ÆôßïöÍo~ÂÅq(‹ÿ’ Ë#e¥ù»¿ý[,ãwÁW”!‹;êìæTlu‡íQsmZ3IZ·:vÖëìæ$§dmqÒœ ª»Q.xeê2áˆÓjÙ"cÃd³›ÎFÖ®7¶ÈecâIjñeÜ–bâïÝu„ƒfA»:‚k×Qʶ´zÇ€´ä ué½Gñïìæ$©Ju§½Û”ÔÙæ&‚3š§ktB£”ìÍT¿Jqb-Õ³›¦ÏŽa<Ô´Z v´" ,“ÃR9ð,Å?t2¿åÕ˜¤à­rËn€C…/|…¿7ÔWdiWÜŒz¹ÕjE¥º“Þ`.*7Ôž}7÷EÝb7”õ3%Ç,ëO´ú&Ý*ñôŽÖÙÍF¸³›ÏÕ¡•ê.½5 ßPbbºð;ZRÒxÝ®Šßƒ•â.ÉÔð¯g7j+”4›ëÒòŽØ\‘&‰‡ç(56ŒË³”¸·Êí–Ɉ¼7ÔñEutvs\½ºƒà¸ÐaƇ~²¡î:»9‰¬%Ùzq’ÍÕ³IâÔ›zM—–hÄnaì6,þ'å¸N’CQx‹‘²·ÊíìÁ÷†:¾è ŽÎnN‚»Hݱ&S&°Oòkœ0)í×ÙÍId-É^$î,‹•<ëÙ-Í?z¯~G@|0°Ûó½l‘ÔbœY‡·ÊÝ» ›/âÆÀ§Ñ^Œ1DÙCª—8ÒêqAŽvßÙÍIb•ê.cm[°Fž/¬@}¿›“Pü’­w‰Â ÿ‹Øíï~ð-zJ'Ó“!K¥ÎF¶éŒGž+öLÚÞ*wìFkF`¶©– n§Ï1 _d`W“¤=ßØË¢No¨Çy9Ú}g7'‰Uª;–‘D^ ŽáÛ%4CÝõ±›“ÈZ’­·!ÖRP=»¿€ÃþòßE‘aL–©ð«ºŠ>/'|ã§(«ààÀ.žÊ•3×@‹ÖrS9´„ŒuÑYÙ´?î`ÏrvvË=;»E(–uTª;© ¤QṨðaö­ïw[Vþ©UŠ»Da†5»=¡9Ñ·ñ 8Ü–×L^J…¤f’ ÖNí^Á†YÕ[ån>vÓìdøö+ÖE®woJ<%ˆèÁB… йpp D4ö¸PGÑn3ă×@;$>|zOk_·‚ºôÞ£øwvs’T¥º»¡ÜÄn8t\ í¥¤ñúØÍId-ÉVŠ»$Sÿ’ÝN*÷ó¿U)tËwp²…º(rü˜u5Pç¾_¾Ï¦Œçͳ›è^£°8"+¡?pÑ%Pô-·¸Yö=¤®a°6|6n0T|ÅýzC=zÝán;»9‰¬RÝAjÔ^Œðüâ¦_ŠƒËX[ÒÙÍId-ÉVŠÛ`±RP%»I寝f«Ï \£¢]YT(K&ã Ì’hæQ‚é­·ÊÝ|ìÆ·Ji¡œ&72"K¹‰ŒQWžL©s‹{:¬fdÇ^Bü5ÊÓ¯½»ÐêQAwÛÙÍId•ê.½u^O¨ UÆ-Ú£¤ëðïìæ$²–d+ÅmˆµTÉndžšÃ€B‡cVåÖGû Î tÆ ¬¼Uîæì&º‡åY`£+ËúNz܇Èm9î8¤Ï»¹ä¾³Û%h]·^ÝApA{¼ziâëŸüS'%E'ÿÎnˆa­¨õâ¶…; ­g7íwÓFj£ôZ¡ £ÙÈé€"Œ&žï½åæÙò2p€ü†Æk¼ zà˜Oõšø#¸ì[,ÀzCmçmÿ¡Ýœdt©ºÃ?ÕlYŸÎnN"kIöRqg%›õ¬g·p WvkU±`á„äp½[Œrxúðcb žlÞôV¹›Ý‰öS`rÄ1³µp Ç…­xfs܇ƒ3.ÚyŠVͳ(>þ÷†ú㛎éêìæ$7?u×ÙÍId-Éú‰»žÝXŒÇiC)¢n,– žb£%ÆI]mUEŸ0¿­ã­r÷Àn§ýç-<á å?ÎÿD(OZ“SŽB bö”%‡10ô†ÚÎçþC;»9ÉÈOÝuvsYK²~â®a7º÷¨PÍ»Á>\èF{2ˆ¹3&æ°•aøÂÁ#Fñê§W¯±IrÍFöV¹›³C0ÐÆ8ÉD9¦Ýð±ò/)u @B%ýl¥ˆ€íW–Ï £²ÙÓêR‰ŽâßÙÍIR~ꮳ›“ÈZ’õw »A:ǘâÂ!ÂB—T®ÖE°äá«sÿæó¿E…â™E@‘?ûá_)TëJ‘‰ã­r7g7m‘ ¤L”ƒSc WÚ|A4&Ó‰ "Q¡Ìþ‚‘¨BaRÃìé u6‡òììæ$¬uG—› k’&\øê †û.bvvsYK²5âÎN«ÍzÖ°›rNL²!·Tn‰ƒDgÊÐÏŒ>´ƒ¸¸I½áO¶µRd¢x«ÜÍÙíÔøÑ÷Än'ô Aƒ!¤&ô"3êvú«Ä‡…¯Á’É”(Ãði4ùxC]zïQü;»9IjVÝÉ¢ŽÞà¢þ‹¶pÓå6>îÖÙÍI^ÉΊ{–ÅJêÙ eÙMCF¡°}¡6±­1ú‘ÍeT˰ €õ'>„zkFöV¹›³ 0F¦ÍÒTY Fï³m êÓ^øó–È cc)²üµSfL^ø¥øÞP—Þ{ÿÎnN’²ÕüE¥ ×pîÚH°` çLŠìJê®ÝœDÖ’¬-î’(küëÙ sV\Ím¡$Y•<Ù^vjéða&ä)ÃVFo•»vèfÄa7pÖÆŠ,Ú ûÖÇ=nŒ…í?"ÈÔ|²d–ûÞPÛùÜhg7'ÙêŽùÎ6­CÇ• aÐ34±^?«ÄI.NÉÚâ®a±RœzvK‹†zDë/PŸû#úSºÔV¹„ê"™˜l.Éàç­r÷Àn*;˜PXý–ÐÀÄBœ/~,—ÿÅËôQo¨§o<–Og7'yÙê^ƒÅ~÷· ökß§oa‚~Ùuvs’‹S²¶¸KÌUã»Åb2AF튷Ñ1좊ÊÖÚTÅ#˜.í˜1YÞ*wìz[…rc{äTÿ±{˜;ô‹Mf˜D‹°#žîß¿eO\Øvñåg: ¥ÏêÒ{âßÙÍIR¶º»1‚C¿E+¥Ö“ÈÒÎ ®¤úºeÒId-ÉÚâ.‰²Æß‰Ý vsYK²‰;вÆáÁn”T悚MUDfàÙMÃ+o•»9»i =R`Œ|N, ÅÊ.Ææ â6Ôþ‹ÿòSMºñ½¡6¤|ˆ ÎnNbªTw´æôµ’„šÏ…Û>®¤³›“ÈZ’­w â8±:¹rS°`ŸŒ Åt÷Ö4o•»9»Qdæ+ãØmŠÀÈ@0çbœáЇ–Œâ¤·ˆFƒ;™…åN#D·7ÔñEutvs\¥ºÃø R ý´a}·}7·“Pü’­÷ˆ¹jnØ-Ìõ`ÔB©N ®çÑŸ)¤èž:Ò¶ð)Cßò¬·ÊÝ»QL}¢ýÅId-OL”qnŠôÉG‰3L.íUT†å\§øXr†î®‘¸7Ô¹Üɯ³›“´*Õn©rãÐ<ûŽ'¹8%[)îTЕîzv£§ÄrPsCÙÂ2"PŸŠ_¬쮂¶ìÍÚÚ8ÀoXûôðÈSèÕlâxz«Ü°ÛÐÝ.æÅ¸Ív!¡Äa­È´n»GA| ˜èº»ö Uo¨K">Šg7'IUª;m–A]@gšƒßõÝÜNrqJ¶RÜ•Œ–F›e7Ø zÒÊsÙ²^Ñá§"e «é6œ‡´äs9ÙÈòL¹ &)—Ö¨ß[ånÎn‚:v$t[XäŒÀźJ!©ÕªñYy¦¿±[‚€Pz¼¸;ÔiÆŽèîìæ$µJu§c¸¨ÿ2EâàBí¤úmäî–I'‘µ$[)î‘(kngÙÚÂx …V,·Ãœ¥ß»©˜$ MÈýù?¦Ö„Åø.~ç´F½Ò‹—(IBѺ¨edƒ+jÔéS„«JêœÉiœèãݸ6d7ðϧ„¿á C5ê ‚>¯ g¿€ƒB`¥ âà²?¯ã õ´,Çòéìæ$¯Ju§¶~…Þ»1Å—1€••’JˆŸa(á$ßd+Å=eÍm=»¡lé&Õí« 'Õ‡9 ‡GŸ "2<¨uZa é­r7d7ì„Ø{¹h¶ÌˆÉ] @t –"c–Lcy*é0é&Þä-ҥĽ¡.½÷(þÝœ$U©îBßoPôèX–¦ ·¡÷n€ÝÀœ{,fˆÕ/]Âç$×d+ÅmˆµTÇn—í«Ò®+¡G¡•“TBcìöõOþ€ Êy– ½Uî†ì¦*P µ!Ö(:iÑ=uKãy­”F¾ñvâC—°!dV•×ðxCÍÞ<£z9PžÛ³ºB­¨TwZF]%]‡ÿm°›*›Ž5ˆãŸvÉn•B¥¸ ±–‚fÙíŠ}UÔê–L:¡:½Ë­1 `¼Æ¨PØÆíÆ%¨½׿ìÆè¸XêÌü#R‡j•ÐÀŸÈ1™}Ó‘éFd‰&ÑÜÙÍÀj6¨³Û,D×E¨TwTcnz•tÝͰFHT+ª^ã—c¯Ãy'OUŠÛk)h–ÝxýyѾª0ú&ƒ4(C9³ã¸„¤öÇ£º´m¼ÿ›g7Ê(£.ÖlœËûµ¿^ñËc1RVçA+йeøf¬±ô†š ú¯³›“ø.Rwtíè bÄAÅ.):ùßÆØ Ø5×»iøæ$ˆu’½Hܶ|G¡³ìvݾ*M¡|sZÓþþ­ß[åî„ÝÆFì“óht\¹ÂÖ€¹?l˜q¤lÇõ†Ú~ûþC;»9ɨRÝ}ü¾›ØmX] ß#¸›a7GuË2é$…Õ’­w â\Ín –O˜Q'éw鲿hæ­r÷Ànðįz¤Ì¦hÃV!M{hÝô]‹‘é9¼{ƒNB×þ¸d§áô)o¨§o<–Og7'yUª;µÌ>Zý6Ô MàNØbÂnü:Iaµd+Å=b®šÛ ØíýÛwÂúÉà0#+Iê#³@“GØ0qÀt°Þ*wsv£€4[¶¦Âõ ÃŽkMNØ|vjü}ø¾9ž*í|AÖZDÍ#Ú"Gd8´„¶7Ô¥÷Å¿³›“¤*ÕÕ˜ -'vöLJ¶SR}·4vc&‚µ%ÆZt'é,žl¥¸K25üëÙMv&pYE©A–QÌúÈZU‚fæú!ÚM`,°ôV¹›³›~œp`öíGߣÁ¦RØOkrÎßÝÓmÉÈL€Ò"øtˆ&FµÿHÓ4£Ûêø¢ƒ::»9 ®RÝi·&}c¼FËÅÁu'c7À§:‰`Íd+Åm°X)¨žÝЊÔc° µÈ@ >²Ô¸Ö¸ÒéÒn¯{f7í`º µÈŸ5Ï%¨! ¢aœTì™Ü’B)>þHœê¤¢ÎRüÎnŒuv³ñ¹:´RÝa“”"âvSšÆRÒuø/5vƒIɤ~—uЕ p¶\~Zb y¶øi„v-Q/îT”5¾e<ˆVG™óküµƒi$N_½EÕ+,݉/ÇÛjPeøZÊ2 i¯·£oì¶³›“@+ÕV•Ä©·è(é:ü;»-.²v-Q)nC¬¥ =°³E,o–¶ÞõpbÅÓT°Äü1(u@|ôXÀC'‡.1SéÖbËQäöz;JðÆn;»9 ´RÝ¡»h)²®3¡‹Ù·’®ëìæ!¯v-Q)nC¬¥ =°UT&¸5¤Ð¦‘8A~6‡úÆE§”6«©7cÞæŸh+7GM§MÚ½RÙ;»•1ü;»à´Uª;ÝŒõÿS¥×Çn-rÉ>Û®+Å=•æ¬ÏØ­'­6Ó=Ÿýð¯ÒÑͬU ² 7„Ù•VøÇ§ŽÎnSLf}:»ÍBt]„Juúu“ï»q°¡ônÝ0Ô\e«¹N.Ù§:»ea‘'à0d‹ë$³@éãí`¦©MݛݴßM»æa:íŸæ3úÀnúˆ>úÖ6ÅÐJGg·J ÒhÝR4tW²ö Üè÷~Ø)‰ØöÄÿ¢¤Úr¥¸ ±–‚6»‰ÎP‚”ÍÝöŽ­v0mÙmÎndïôœW¯±½Èüb䙉vÁ+ßÁ>iD.uv+!cøwv3Ài ªTw§ƒX‡Ín0]¼Jºÿ»¥=ÛÀ[žmWÈ•â6ÄZ ڜݰ§1y”îvÇÇøkÓHœ =°Ù¥‘ßYºO‹-2O‡(Õ|/ >›2{Cß~PGg7'ÁUª;uüŒS%§Jï6Ø5xìßábá†/¹K‡õ9É(&Û®%*Å=•æ¬O»q`òäÌä‹”g„"ë`³›ÍhéSí`¦©MÝ;a7àU¥å’zpbq²c±š&®#—A•ý†1‚ÞP^w¸ÛÎnN"«Twb7}·4ì‹>cʯ¡ônƒÝ˜^ÇÀÅ1jäà—/¾9‰ÃN¶]KTŠÛk)¨†Ý0íbà §Ï¦¼{£ó ³¥FëT¨ÊäŠ:Þ6:ÚÁ´3°vÀ0•0Æ…ƒkçyj°[}âÞPO³},ŸÎnNòªTwb·Ñ¤·%]‡ÿm°[„½[& YTÃnT6ê {¯k–öaESõg”æùœä4Ös÷%ŸËñV¹{`7@f™ ËKµ…eÿ/^›ŸƒfÞìVŸ¸7Ôf ØÙÍIH•ì†M2ì‚™\†Æ»1vc¯ëR%ºZ”íZ¢R܆XKA#&Ê–‘·#öC~YÀ#lÂ*ìJc ‡þ”UM¿üKáÙ”åÉÀØP<ËeDnÓHœ ­Ù-œÁÑΠ¾¿v¶G¡v»,qo¨Gy>Ümg7'‘]¤îØïÆWKøåÄžÙ½oKqŸ–¨QÈvô°ß1}ñ-¶£]K\$î‘eýkÀ¤JðÇd ¼Ã$&sšAåÖæÁnéh_ýjᮤ‰>¯ü¶N;˜FNò«·àŒÆÛYÏOO€×3#Faµ0Ìx*4e·+÷†:›óyvvsV½ºcÒã 5~Õ1Æ'«èäi7Àúâøi‰…\ŸÏb¶k‰zq’ÍÕ€I•€³`4piQZê@ÌQš Cì±Ý’ÕÚZõW’K;˜¥”åïWoÁßn\D(¼t!pð+‡^¶8Sv»"qo¨³9?gg7'aUª;Ù|h(~ãeŒàìX_?-1RžõYÚ*f»–¨w–¿lÏ0y;5‡®QÍì¦K®jØÐÞ–Fžu·ƒi¿Â¯Þ"»qÅþƒzúÕç³íWˆÙ®%*ÅmY6´L­P¶¥ñÚ ÆW´ƒ“Ê:üêí,»eóƒ'È—‚Jþ¬H©ìQ‰{C]ÊüQü;»9IªRÝi°F³‚æà5Í1 —ÕuW7Ài1ý´DBžægCŸv-Q)î’L ÿ0§‹ü9ÜÌ^ä?,b){ºbM{I^í`–R–¿_½½¶qÍ|èyq>F.wE‚\´M!†Oíþ¼¡.¼ö0ÞÝœDU©îN#µ_ýyY–8aã8CÝõ±Ûâ"kוâ6ÄZ ªa7Ö{Ð;ÒHýª"@Q͈SÙHgÔæ4ÍÔgìÆÚ’hžµ¼(2%eþÕeáÌ®‡GÆwHVºSäö†zúÆcùtvs’W¥ºÓj4àÒß’®Ã¿³Ûâ"kוâ6ÄZ ªa·° òá1l¾·‚­Hu2€ •mx¤&²‘Î4¨Ìiš©Ï†ìöÕþ”…©ðè¾^ZÞ8QäX@%Žé†WðéVòàà®!uxC¾ëˆîÎnNR«TwTãx¶¤aïÛÃcI×uvóW»–¨·!ÖRP »±k’Š„âE2ð"¬^ÐV +ÊKÌté£1¹SJ¤äßf)eùoÈnꋆÁÚpJ [æuæ4ÏEŽëU™qt0,ŸÏ/2ê¹7ÔiÎèîìæ$5?uwì&zB©Òxѱ̤3|0–‡!&"‡ £Ð`¼µò)æðeXÑæ¸v-á'îvãÛÙ°U¬Éôÿg7a‡AŠ”+D~xŒ7:ÚÁ´3°7vcmü´«sB+##]ê«”’õ†ºôÞ£øwvs’”Ÿº» v“jÅê‚ZFÁ¢ÎËÊÑÃjõ̶óJ$ALâ ‚±n8V¶k ?q×°˜PØÑ|Ò[NëJm•†F7ÖKÆËñ¶ÆÑ¦ý–½°[ø üÈŸ¾Ë`f ¼–§ÒÑ3î0O»yá…?o¨ ¯=Œwg7'Qù©»Û`7ÁŽêFYÉ­™tÎpÈJDSEA'Ÿþž«ãùnòÿ4ïöåg84Ž£Û<‰uòh×~â®a·R¹ lÂgÓep=+›'6Þ0þ ú|þ¯Lû{`7 £¯…Á&ÂÁeÔ.âœ, Ê֡fŠÂïÞ š t}ÌÈœ\ö†ºËÃxwvs•Ÿº»%vcì¦y7¤ %`ˆë™ ˜tƒkös¡“Q;˜1N‡6é‹Úµ„Ÿ¸g·‘M³°a¦² «&1ÃB ó¯L3ù-OQ>gì ¶’í—`rœƒ2ÿÁ Ó„l8x¶dF@Öútq´“g 6ô†:S˜Cyuvs—Ÿº»%vÃzmb²Râ 2jVWÍ~.âÀkh#Mµk ?q/În² kûIjÎD÷5.ëqX“Yþk³œvÙ|ìÆ°9aórÇQ",XŒ1¡,& YÿðA&RÐíèW¶tÛói\¢Î¬hxÖêQöwÛÙÍId~êî–Ø-]¸Ž"Õ«Ô–SIѦá§>#7$h×ÒøíZÂOÜ‹³› N²ÒÌÜ6aFjçÁZ0c‘CWÛKPÚÁLE3uoÎn05@C¡1¾ËíÔ¢ŽjU7€Á/¸ñ²“O •ú¤¢Á/ÕIðÓO?MŸíîÎn) ºýÔÝ-±†´4E«#ž©ÏÙfˆèk‚>y?GHÿ_¹]Kø‰Û‰ÝÐ̑ݴe8Å.º©lL1é†MRÓpØî6´ƒßžulÏnœUòðˆå¥ªPuþšf•튄RW±ýâÐæ \ÓÈ©·(šÀ†åå¬ÞP§¹:¢»³›“ÔüÔÝ-±›;]ÜÓÞ¿åìM>4• ¶òü=ž½(r»–ð·»E“#XaaK7¤ÈS® £‡G&Œ¢ 90u·ƒ9M3õÙœÝȌ̆2 ”¹â‹6*&‡Ø+ŠŸ®(…Âêô]Gtwvs’šŸº»)v–|h!ÛiŒðâå”ݲ¡fkÏß»$2 ]Kø‰Û‰ÝÒjm©§Ü¨V­ˆ˜•|ÚÁ,¥,ÿ=°Û9‡Ø¬§çhÏþ3×ùì¾x7¶ðœ7ÔŬ$ ³›“ üÔÝ-±[\<¦•猴<¥h#¹h:^G£–1 Ç(šn¹ò°>i×~â®d7Š€9W6[~Ó Í,Dx2^ÆðÅeï­H'æìÖ€v0Ó7NÝû`·'@Ææ E¼WpÜ´\‰OmâÞP'Y:¤³³›“ØüÔÝ-±Êm‰–¯Éö…YfÚeұ[åù{E¦´k ?q×°ùbBÔñMLÙœ†øƒÕ‘_{Ë6 Hw"Ì £Õ´ƒi$NÐØMŸ§ h Çæpkç9¡¿'ð±#×'î µÏý‡vvs’‘Ÿº»%v“vÅ2Y3§£é'>¥hÈûO0xVÖG"íZÂOÜ5ìxçá ¡u.º”‚+áCBÁ“·¸c«¤uñGLDãPÅŒÉít‰`|W;˜1©¬csv“X™ƒ ]€D+Ä4ÏHèèN°’µ%3}ƒÁ¨^™¸7ÔÓ²˧³›“¼üÔÝM±Û«×4äJЖù üË÷hr˜Ë~PªžßÙÈ$Û¸²ÚOÜ5ìIÖ„"—¿ &Ë-èI?kOžY<Y›¼¤Ãu¬¾ÙÈx¶ƒYJYþ›³[Ф/²+¡Gžõ…bä†î\y…?‘/JÜj[ûíìæ$#?uwKì&q&óò˜Ô²=aŒo hÑábÕ¬—jÙø#½C³$u>VëTÊŸüàÏÖ‰ª~ô=±›ôsiS•¸I‘ ¢aèQZ`©w·ƒiHŠ ÍÙºŒÅØ+a jJ]ʶÌôâ€15«tJ1ñ¿(qo¨|"¨³›“˜`7VJx\·Änh fâZ9𜮙DLøcØÔªhÑTv(ê1Ÿ…àZ”ÿÚµ„_g¦fìF•œ¡Œ§%|a‚ÒÛÂP`ˆÅŒe¨aö\d,Ÿ\ᜨá<´2–·?v£ì0€sgÓ=ñíÎÕÌúÄÛëíèÕ7vÛÙípmeÄòúõk²²!£ Ú0G6Ìч5“9v£!ãÏT67®0Ù‘Ûåª-´±ŒrDkÏÈ?Þ¶k‰mÙ Xt@Y<ñ ¶²M¾š›Cô3ÊyºH5‚ƒå,Ø äcüv0cRY‡_½¥òTvD#2W$«Ü^É“-ÎÔ3›ø4šÞ¸”6ȦtÏÎn‡“àRõÙOKÔ³±h¨Á!ccv776IØ z†fº²ã;±[Œ#‡û‚nWÈÛ²Û©hïÞ¨¼èاÅy±g8 Ÿqyæ“»ŸÔû³þUz;r·ƒ9JptëWoëÙm”%ݪŠfƒœ<½¡vÊöjÉvv[ ê¥^tKìaQ…Œæ/pÓÎŽ;Â@OŸ.-½”¾´F1ŒGäà7/2ðo×›³ÖEÌŒŸá3+¡Ÿ`~<…éNæ1y„ “房FXë®”Á’ÂSüR|{, ‘ÅU7Ñ‘µ k7wZëì~E;˜†¤ºyv³‹Ÿ†zC¾ëˆîÎn‡“ZªgZ2ï§%*whP€®9¯ï¢ÈÀB%¬ ¥2g]J ®v-±-»Q.$1Øb×¢¼F‡_ M–Æ&©?)ÛŒ–Fn3Mmêö«·­;¾|Ocšá‰O±NbÎxxC=óúÝwvÛ½ˆÆ¼v‹38°Ûìy}E2ö¸AˆÂNG#ʼ]KlËnlࢰ9^˜ÇUçù=–[Pâ &ܹ?E&ÍÁÌkÅnÓJ}c·'¶*Ï£:¶È@†ãæNFc{-«]öQ¨7Ô£×î¶³ÛáDv3ì†*`HUy^ßE‘‘))£[øn š‡Uë A·k‰mÙ5!²IRR”'sj,+£ÈÀN„p ûãâ “ì#§ÈçÍtväv0³yˆž{»¥ÃDdjäB²@(\8‚Ѹ9°Þá u}Nö³³Û>åbäêfØ2jyí½æ¼¾‹"‡y(múVÙ*¥]KlËnXER¬ç—I6ø˜Z5‹dÔ¬-o€P¨uODfnŽRàGé¯ÌRÊòßÝ·2†•‘œŽ“Ö,éCºÙÙ_f|JWå$&e ìÿðȰBæ†2¸ ^#3/~P!žÙøÚG×Rê¶™ÚÁÌf#znÈn˜ÐJ«F:.[*u$8µRWÒ‹@î ·w²ÄrV8¼¡®È®£tvÛµxr™»%vË•ïús¡¹‰'õ®9 %û¢èÙ®%lvC!3Þ1®µá_ÃnØ`U+èÀã¼£Ôb(¦#µžGd§)#,rÚ;ž˜¡¡J)2´ƒ9ÊÀèö v£Sa œ‰ŒFoŒ·ÀË ˜^ŽxÃdRcøŽ,ÌÐ"àð`L°Ñá ucö6¼³Ûæ"¸4÷Àn¥Îp=V‘4Í!N±×B´k‰Yv£DÆ•*Ø‘»†Ý˜ß¡°Ú¼¦ýk¼‹‚S®bÊ„¯cåƒý¥&‚#Yïp)Ïò·ÙÖA¹tiªkô;‚wtk³[6cƼµ¸‡s‰‹ÿµ×Ûų´«;»íJ5™¹_vã8Êý©¾væ)Áè4”0J{Ôm»–¨a7N!3ì¼ÇA¿x&b¾õ'òéØô¶†Ýx;ö®¸m Éòk°›¬µ<ÅЃœ_À!¥¬8¸íÑG;˜†¤²ÙP£APŠíÔ}»±´³yfÔ\T?Ðöøó†Ú#Ïk¦ÙÙmM´y×ݲ ÑN 4¨´‘•ð”å •Â#ZhÁƒ®SE5ìÆä‹Ô)òcké¨xkØOÒ`f„ñkÖö 4#P„_uÉþe»¥ÈC $»TÍæÈÆ-ýy˜ Θ^93ö" ¡lÖQÏn ­Á2‹r)r6«Œv£E÷!œ·svÿk‰ïÚÓ`û ÇÎ#sáÏêÂkãÝÙí0¢:gt)Õak‰lÛ¯ô¬QÈçÒdþ‹›¦šÓÇ &2 6#ŸÓDÒW_ñHÜIG»¿]K\Än»Uâ_&IA÷ T)ïÇ2šÄâp>ÁUø¨ â&YSŸCQkמß2qµƒ9Iò™‡â“\\C !« ÛÈÓ)‚&f'à*Ù (Búq‹D€´SC$HpØ_X`’Žd£eGkyí«7ÔÏp?àMg·Ã ínÙ ƒ˜Ná`¬“ZIv´z4Ê„Þ; Mº¨ÿv-a³"“²E¹I¿q‹gMo¡†ÝÐÉD£qr~·9ˆ )Þ’Ê ë%Îû)Bþåæ·¬oÓA6» Rú?¯atLG¶ÁÄÆ¼’ÝH‡a hsá åì2›ÀVƒÅ8 7}òSF/î  zë²w´×[꣇vv;œï–Ý.’ÔI3ó,p\ifDi¶k ›ÝШwú}x´Õl ­a7ÞŽ¾Ué—¡™¡E)µÔ,SBš*E–'eËà—[ì½ZÌ»·Yí:õ¬a·0Fxõµ/{ŒŒ× 6Áb«Ð¸}àŒÛ“=»×æùEùÿ6»1TÇ £a„ñÅhá÷ÕkÊÛ8 Û Ùøê5–FJʯ#ýl^áVu3 Ý›ð¬i.fŠÛZbªfë}jröçõÙ«òrÏVù•fôÚµÄ,» =zò\õH³L:ü£>?C K+_£í µ¹Ü€ eÈ6cF´ƒ“Ê:ìzKhà²óË }‰ÀzÃnzüŒ£ ð,)ƒhàЙÞ5Š©ÛÐëxñë"·Á˜ðê5ä-f#ã)©Ññ ÿÀªñ&&ÖR|o¨Kï=Šg·£H*æó–ØÞlœ°0ö Ų7:¶b7D†Ê2.CåÖ°›V/ ly… ‚’·–à’m-­Hvw‚Ð8*d„Æ.¥Œ¿·Ê½ˆÝÀBI/j‚lvƒ}4–þScŒÅ ×ˆB1"+šHî¡ðëmQy»IGg·Ã‰5UJ-™·µ„­ìÐY…¬µëðz²æ¼¾–b¦ÏnÅnŒìÔÃ/ýx΂IµÞF*7-¯á&òu›°lÞäÛ²Û jf9u1ˆ{î6 &Èf·ž€™ aàýÁ_#ŸÑ- P®YyC=ÊØán;»Nd7ÀnZÛkƒæ™=¯o)mÅn'sÙ«×ôä5#ÍÉ­_CåÖ°‹B¹R”è9dgÓ8W¸±˜…¯½˜'›y«\»W†á;¤qP_ÍnW i>ò†’ B4f9½¡63y€ÀÎnÒó,Þ»Ñ/¥û¨ê©›ŒiòzÃ5wÛ²«æ¤Z5‚³Õl ­a7Fß4/¦_L” ‚ÉClM‘l´feà­rmv7ͯ•~#°YÇ%c·xÐw £,×y²†ÌëKôØ‘lºÎ$MÓêô]Gtwv;œÔn€Ý²˜ßð¼›Æn~ì×Lje sd!fq¾Ôí?ýgÔ,&8uBè'”ÒñV¹6»šÎ²MÝYR‹ž•ìÑ“2ãeŽˆI§;K˜ÔûË\ÏÌ9‚ã$µfSð†:ûÒyvv;°”Õ[e7–:Øsèí’*-}o×öšI±»®Ð¢,ÞC_q±v®fýdåØ µL)8y Å PöšÉ‹hJá¶>tαÀ¥DÚÁ,¥,ÿYv¼¥ßHdYG%»i¢‰Ò¤öÚÙ® ÚÀKþyD_d˜NÞ)5o¨+ó¼ÛhÝv+šRÆnŽÝÂþ©0¹`Îæ€mY˜ ÆÅტ=}ø1Û‹XÁSæÁì€v-a³"“²Å¦GW_n:ÿØ ¸å7«iåYÃnŒÝ>PöðÈ(¸dƒ-U*Ã_ ,™n#Ûˆ‰"004â·ƒi$NP»½zÍBz `a"zM\‚Ý€š Jv£a3d ékçúP¡-Ã2ªÇ³ÓÕž'ä õó·﮳ÛádvSìöåg©¶7&Ðiélö =1%ÁS\Œ#JâƒÔˆ ÃŽÀqå¿v-a³j›¡´ëé—å4ç=Y8 •[Ãnèd(BßbËÂ\$[.ñe!&Îëpð.ãùv0Ä ªa7xã Ë€š Jv#¥ñ”ùšPQg¨0ÃE.=å ué½GñïìvIÅ|Þ»¡'ÕfaÉy=ô£ƒIvÔT8ðáÅKÜŠŒÛ0‘2ÊmÏEÊÜÇä&®v-1Ën!G Ë&V0Åhy*·†ÝHMûÑJP8RÖí¤ Wz¤ëÀ‚3jÓHœ KÙ±»Ù™l àá ón¯^—Ró†ºôÞ£øwv;Фb>o†Ýh›0j_EÓªéçXX9èŠëÄìo¸ñ¤ív+¬m]ŽIù4åRdâ´k‰Yv1‚à‚ ¹p¤ƒjØÙ™ÑŽ e „Ký/ø¤‘#°©gt·ƒ“Ê:.e7§±[6o#ÏÌN¯¾šÝ©ÁSaèùª^úó†ºôÞ£øwv;Фb>o€ÝX¦ÅŒ¢ZÖúH‘ªÔL¨Ôpœ 5DƒÝ ÄKê8qßÙn©…›³fÕh,•É4,ùÎïD"Ë:jØy7LwèxÓwz¤  [oñV¹5ì¦<ô…¨$»1¼ÅÍ•9zÖ[&kðD.tÆ~òƒ?ÇVLõcóÜÈ0Aüî µ"/ëq(¥y.¥7Ô5eÜsœÎn{–N6o7Àn(ô ƒ Ú>ª¦ã¡qÙ´Ô4vB‰OuåAÅÇ‘¥B=Nh˜Vª„9zgcÚìØMëÈÕè¢\ö ®†Ý }%‹S6IŒ±øL‘¼Î‡¤˜ë¬ÜIç­rmvƒÅ(;¼FÃ,À-n@Ö4V$²¬cYv£V¹è°”ÄQ’²p •DG¨u”â{C]zïQü;»ER1Ÿ7Ànš» ­þ6»*(°¦FÛ¯Üú‰`t»,»‘mFjT?.Û6ž05Mg­N#D·7ÔñEutv;œàn‰Ý´˜Ú}^_8hUÅ©:sO±†ÌîA+0H˜æc JÁ¼Ch×6»1þ ™Nߊê”RŸ,–/^6Z&# <9›%ÚcåXªn Íhé‹ÚÁLS›ºkØÁ²pìóYǵì{hÏóË¢¦÷oDz0—A&Ï“æ“=Üó†:ÉÌ!Ý'¶[b·Êóúhã4d8£tÊÅU›Ä¹!Ø%sI?:©ŵýqµÿÇ9W˜¤SU™3ª{CËÝ‘ü:»IZC^o‰Ý(m{I¥A0“O˜V3W¼cæ Ù°Úad|”&5r·k‰YvC…’yMýÀhrÉTµfÝ5ì†NV4 ¢1k&K*wTöÅoÛÁ´³d³­Crg€ õÈÍp)òQäè9Ënt$½2I‡ígóLdØ;ƒ–—0¡&#y6²m£º7Ôv>÷ÚÙmÿ2åðnÙ&OX‹™Uáv„Lz{bí æèÓБ»]KT±Ûðù "âoÔc%•[ÃnZ²Â¢VSÄ¢¡ZÏÖ`ág±ÿí`ÚY©a·éÕp LW9úϲ£6Vï0*LàµòK4FvqpæÝæzò"ã0,™ÞP[;BXg·#HéYï–Ý‚¹éÅK‹Ô‹Ínt°Ñ ô®¹Ð0<ò Äç7íZ¢†Ýâ&¬¨KÃ$Ñc7Ê(:U¹¬R˜,<£õ®L;³ì&3 €ýÎÜ,»±—ëyÃþç>ÉÝû·˜Ô%ã—Õ>‘é’H''û³r½«7ÔÓ˧³Û±äEnï–ݯAm2ÅÉ÷¬øš|¹ûIúÙÈx¶k‰mÙ­T.üõ©;#ÂâAí`ÚY²Ù þ:1Úp`5ëm4é& !}žØµÈ:fÙ5²LBCŒù5Ìt-˜ZåÕÔ[.<;!ÇÅeF8ôÄȳ|Ó`:o¨?æì˜®În‡“Û}²›VJS]%¯ÙÓÑŒYÔ6ôRk×6»!2T.šÁZªTã(#õ¹k,“F¾7vƒž»=Õ`ص‘y‚Ä•"Sá‰fïìh¯·™LÜWg·Ã ó>ÙíÒÓ™­@“è Ýfs’®]KØìÆâ7)CzòP-J^ƒ|åÉ*©¹;»Z¨=vÓ‘ž€6‚‘[ŒCó™EŸYvC”ð&¦d‡©™¼‰³F™Ô-o$~¤j\ZáO-"Y­¤Õ/TˆOšBên¯·ij·çîìv8™Þ4»Yë ,cÎÂ#ú‡98#B»–°Ù 婾èìÙï«×£QFÔ´rtv Îf7àAŒ`ŒKèf”Þβé°v—WÐqb\†Ûø|6TK7&Í¿Æb¬hM=£›±õ$nëÀA½å]1ÂÈÑ^oG ÞØmg·Ã ô6ØÙ†0øÃa}æy}9=±Ú?çò#}‘! j×³ì† e2%Œ"‡}Ráw˜²•-OuvK%…Ûf·°|tX¨ÃÐ8Ú©iyôLI-ºgÙ-X&_¼Tg‰î ’ò¹&rú~ ‘1§;Sà¯q¼ó=oíì3Èå5–íõöüæÛüßÙípr½ v»è¼>É(ÝïæÝ }Z6À¢L°¡(pèŠÁ²ân×5ì† •j…ÑpØj6êÛÎn#‘Ùìnq˜²IRaà¾lÖ1ËnÌߥô$³@©*’ó0øbCÊù¢{Ã#¥¿¹ãçÚëí8ÅÛºïìv8yÞ»E# š#a„Á˜Ã4‡V•°d:‚Þ½ÁQR)7ÏÊDC$néÉ‚n×6»!²8™BNäfNspY5=`·rçßÀäê v0íWϲЂ{xŒãbn¡ˆ)¢Zr̲{.¦ôÄ ÎÈsÜòv¶«Íï¤L®éÎŽlúÞPg_z ÏÎn–²zì†ôö#,Lˆ´Ó’Fë´H•¥×uÜzVv¨.ž‚qäF]@…¥ÏåH»–°ÙP2Pº°k•”-þ5ìfï“:+Õ,ZË{¶ƒiçi–Ý4(NGÇ•ÃdОe7;o£Ð“ù=~çÝúivG‹0IDzÀ{CdãÎÎn‡Ûm°#,†oabbî¼>”3}ãØ­åV>¥±› )¾Üöa}Äi×6»!2†£+ÌÁ ‡¨PFv “A—ì“r­óí`ÚÙ³Ù-“5FN2sk@½8»¹œ¿€3Û+‹¥®´À/RoãKoÒÑÙípb½ v£á3Ï„{Íy}SónŠÌ†)¹Ã¶2ós9í Ùf·’FK ŒÁEÍØ 05è  5û¤¦x.èÓ¦›Ý-‘å_’…ü—»‘½8õÇf{e' üð¥Ý <(yCm bÿ¡Ýö/£QoƒÝµ1ÝÆø¥rŠafÆxžÃ(hzËÌ>¯›úGŸv-q»¡Qg7aÕ°›,Ÿqj‡½O*ÜÃѦ+›ÝèÒP©4Lf ÂÏ÷\™ÝÂÈäósÈRâñÑy©ž'W€´=.xŸdQe®¿ôúX]Fë(½e)]úQŠ;py× ×".%$n·t…†¥õßþÃ.fˆ˜#›}ªF!ÓSåÅúƑϳÛÑWlF·Ï¢^vÓ^êÙ-¦¬qýÃÛ0iÅèóú}R—¡saìv0íVÖ[:àE‘NQ-ù,;vC¬Ë’ŠŽÍ)˜do§ì•c=bzC]_«Kx–ükjµ] jBÁ‡æVŠé^é½Þþ‡.×jìFÅ`žBê7þ2(€æJ•Vþ5U…Ì#®Øg¶]ªÞ=¯&1y ·Fd-´P|Ç5˜ìòO´×[ ²8°ÒL·±}x4ð¬|Ñ>©< ù¶ƒigä@ìÆd1\FEU dBvd/°•ݶÀ{Cm×j£ÒÎÕÔê‹ßz£·x†+úFnR¦ :Üoñ›v´f’[°© kÀd6ç³þ­­-„wã²ëm:L¦ÂЩàWgzk\F½%¨ŽÝÂ*} ÇøM )¸1ãc=Þ½ _¬´UÜìŸ%Ã<Å+dÐà6ÞPwv~ˆ[ïZá Â:ì†RE3ÀSm aÙ˜ɧF!“&ŒÚ £¶/iË€f(gVåGmDgDf¦()ó NË/É¥½>Têz\(=ý–LýkÀ”™wPžI“!@âëîlÓ΢Ín±W¦Z:ýM±ºgÙÒ£ ƒåö†²y¦V"Ä z-EŽ)„a¼üí¯ÅÛ¬c–ݰfPóÓjá݉ÔSî©­¶B.<2,™èÑ%£Îðz,…?o¨gkuÃÏšZ](ôbÞÞè-–Ñ :t¹Öa7š°ô0 å䙵XÆZ]Su¡*Ú~*7(É覦1qe>òÌÞ“Ëþk¯¶€¿„< çjΑÑd„?G覎0ÃÞŠáøMƒ¤ Œ!ñÎnñgà)Rp“)©Ï,»a릃—V' à$›e7ä>â&$EäÒp”,ù¤KFõ>¾ZžŒn¯·iA¦n»V§¸]ꮩÕ1?Úˆo—rx£·T>/MçÐåZ‡Ý¨®a%Þ0L‹ì©ŸW¯^Tu/’ê‚Q ÚÙÉ‹‹’m¯¶@gÜ®ú`t ˜2ó¢á5Fü¡?yãE ,¹L;'•c7‘µE¤¦šŒl=<Ën%50º-lÆ5 #>vd†Ž‡Q@}K=œU2¬E¡Ó‚•²ßj»VÛHÚ¡5µ:–:ΞGŸEÞè-’É+9t¹Vc7ê'+úhŒ‘Ëp3ôÀ¶`Õ­_˜ò¾)³¤Žå(åÅqvã ‰/j¯¶Ð1´è)nê0ÄþÃ4B*w˜ Šed¸Êè8Þ®éhÓÎí,»© , }³ø›Öä)Îò™e7†T²Çñ½ÔÔ ¥õ¿ Çf- ¡zŸO ‡ ¹J€xCm×ê†5þ5µ:ô ø÷o1¹ Ö ûSV% ²þÞèe_º‚ç¡Ëµ&»QQÑÁ20|l”_ngko]Õ½XΨ)Øvxì ö/‹!ÉÏ4-²ŠZøïöï1¡èè`OãDŸöú`ë!ódc`J÷ÆþÃ(·5`bã-)ØXÆÕí`ÚYµÙÖé Ž€z 5|l[z„}–Ý”7ÊÔì°Âß`+ÆÑgóûñËݰg%&AžÒ…X©êÏ‚“o¨íZA»ÂQS«éE#8} ääxxäèÈ€&§7zM™kxøÐåZݰí„yœç`(²³ësMÕ½Bzä$îÀÇ€ˆDð$ŸÓÔ4&"'„ªÌ×O£É§½>ØzRVÀ“ü=`Ìào~0ºLHœ+-%¬g©ßJîv0íŒÚì† 5¿P°Ä `WZ…Ö°FÙ¥­”Óœ3Ñ–FÖΗi´ç>O2c’ø Äh‚xk‰ÕPãŸßÛµºÏRœšZM^ xzôOˆ/ºžÁ¦;ïŠÚ”¹†‡]®ÕØí£mG»o’³Jl‚«¬º— :£½£.P/(´º%–íÎa¢ÓK×  ªÆëÚëìЂÜ">5ùÓ$æÜ£kÀ„ë9-V%gÊX4*4ŠìÔ¦7›ÝÂØmX«#Žá^j즯)Q£´Îc#‰Óòy~ÿ6t`ΛxŠÈæ£ËF;aØOÇÊÉRlo¨gku‰¼fýkju,5Vjx¼]ÊáÞRù¼4C—kv£ÁJ?h¶bzVIƒLkòEU·^vAQ ó¡íŸ/ƒ¶ “Ñè¥íõ¡F‚{xŒìn_½fg `êS&oÂ&‰cTÌunÛÁ´ói³Ðaæ¥V€­*pü¥kd÷Êxvv솦Ep©šãY«cØ’l{¡\!caµOy86ìw£WF_Ž&†£”8©yC]S«ÓºZﮩձ€02·K9¼Ñ[*Ÿ—¦sèr­ÃnZÁï¨ÆRͤ.Ög7=NIÈjq©è³ñÛ냭I1ñ‡®›]3B›Û=€N&lÎ…tXe××Lu–榧>³ìÆš?ªE`žÝ0,¹d#+M‚H 7MO*½”8qÚëí¨ £[»V§¸]ꮩգÌ,~ëÞâ®LðÐåZ‡Ý°tѬ0Lë­Ljq 2àTuëÇb•Õ Fk¯¶ 0+c#£öÖT]Œi$¨pÚ ¾Bší´×[ˆÝ"zSG#˜¬#åbÈ–ŽÚꟷƒ7N¡ÌqŠÏïmvCSxSj‚fÙÖÇÒʈ)’‹Å$\Tàçy<Ýéº„ή“Ì>>ëé µ]«m$íÐ1[üÆÞè5fïêÇ]®ÕØ {—Ö0§ÊýÏöª »ÁžôãÖöð([|d·¡Ã¯núkàY£“ %°÷BjaGÀ0tuiyлqÙìFëˆÕ•‰NéIÕÕn™„þè•10ÄfÈ/Öf/ÕÛPɇc?y» œ™Îj»V•v6¨¦V·Tšg½Ñ«ÉƒGœC—k5vSÕ³¨jVýÕT]þ(šÎZ÷÷lšíõÁÖb·¸ª$Å0uguÂ¥lYx{0Kj ¼ü¶«-cô´Ù ±æ ·M²Àf=gÇnØ Ô䘎" >¨©äJ 6lxbˆ]Š Xïh¯·ö»ìZ°Ò³¦VÛykõF¯=‡×¥pèr­ÃnL´aA3”~j\SuéEs¥âÓ¶ëÔg5w{}°õ€ØÍoUIì*°º¤8‡î*ØrŸe7ÕLúc‘ãd1klÔØ4Ënä R ‰?<²¤DæÆþÍsèu ËøN—ÌûÙÈWx¶×[û¥v­Ž ]á¨QÏóƺÓKOŸMïª"{£7ÍÖ:>‡.×:즆IÛ,]F•®©ºˆ`Z]Ói£uj‚ÞÒ^l=Ìe˜^ ÐJA5`ÂnGÏêq”'Z”óÊâ¶©5a\ L;Ï•ìž=°DŽc1}¶ÎòŸe7DI²ä»=€KãðÅØi¶‘ B×Âý†qea 7}|Ö§½ÞÚ¯°kµ¤ZS«Oy{ÿ–‰Kõ †ùM“ã.‰ìž­_è¡Ëµ»…á˜ÁóñW>ïQ{kª.}iÕXúÕ‰ Íª›Ä3œZbñ<“½Ä¢½>l«ТL±‰[*vcnA-úÌ™»v0íØìF½J—¤²•!sì¡AFF½%h–Ý€šz‹šÕ¥”I–RO³MjÈEÝ+4ô:Êßk›¦`ûxC½m­VÙ‘ ˆiv·&EöFÏȧkС˵»ÑÒ™q‹j3ºå0´D »Ñðµš:.x¦á?gº+«)20;i¯Ûêl¼¨Yt,%µp¬úйÁ¶ÇÚÁ´ßo³‚ˆ\–uõ¶†ÝBúge+•‹•¥žf;Èe8;Ž)×Z³# F¶ÞPo[«);“•…#Œs;Ì`f€¹(rHü«¯–Ò¥™Ülçuèr-%[KŒæÝ0¿Œæà -1"—¬œ5Ü8}Fdør“ŽqÈF¾Èó”òùã|¦d6åöú°­À¢‹¡ŒK¦]¹Ô¢áߦý:»ÞÒ:0—QokØ T`«Œ Q.N²ð†zÛZM5з\ÕgãVÝÔx;ª'EæYoôFÙ[íöÐåZ‡ÝBµ<éF¡%jØMé3ÊH/’m¯W¤Ü^6×í¸-•B;˜vNlv‹Õ2bKÝ1BÖ1k™ô³¨Û¥Î†zC½‡ZN&:Cwð¹¥ÔY4ð¼(²7z¥Lzûº\Û°Ûd£VV9ȳ†ÝaÑÁ¦O«¿ÆÊê‹êÃ)·×‡=è‹Pò‹Ü¦7›ÝhšÏåCwœÛ£Þ4ËnS‹:C†E,êv©³¡ÞPï¡VCX\p`ÎB!Ï‹"{£gäÓ5èÐåZ‡Ýhé°O˜¾jÓµêó»È-´ G[%ë¥þe™WZÂòxs*¿½>ìA¤EÞÐݦy›ÝU-ý6²}§Ðy»Äîm§%Ôê=Ôj-”¢÷ P¸íeÕEöF¯E²-Ϻ\«±z€ùÜx\ ‹0ØÊA¡5c7(R“òph‹(§ÏžgFž…¨u<óJnÚëÃô@R -í`Ú¹¯b·W¯§Soâ;»ÏŽÝ4餿ô°†g ‹º]êl¨7Ô;©Õ,Ö UYRÏúÈÞ襹ZÓ}èr­ÉnR,ýŠÇ€‹å%¶Š¨a·ÜºýöºýËj½ëôot›-1¿¼=ð¼XÛÜy7®v›nœÇæ°»MíÞZÈ· ÖÞPï¡Vk®-lZŽâáÖ€ú¢ÈÞèùt :t¹Öa7´Áè”’tß[Š ‚«a7´DIGËéhDFš°0éëÐEnÕ^ö Œ®Ô¦ÛÍÙÍÉ¢n—:ê õj5-—¹6 ’\8¸5Œ“EöF/+²<]®uØ öQ_·ôÛÈn4œ0©çv4â)ÛzÅ0{hÔ«öú°=`pÍ v0íÜÚìFë@ôh9MÇZöù3ÈÑ'똵L2³ƒE³§½¼Á.ÂR¡ÞPo^«µÈ?~B´wTFFÞè-%åKÓ9t¹Öa· %&ë$SŸ¬rgåØMÑœŽFD¿qꑺ|ÌÄ¡‘ŒJÒ^6×FéVjÓΰÍnÌ‹ÅaaaÀþ¯±¦QžT6£Þ4Ën7öÕ]êÍkµèŒY åS_*ç@Úl¶/ŠL Þ5›É<]®uØMJ ]9rcâ.-2©d74‰ÓшÚõÉTçºôdÏG·×‡ÍõÀ¹¤ÛÿoÓ.ƒÍnÔÛ°Ö7»UóÕë´gin–݈@Ç)žÄ…åuvžB½¡ÞC­fÆ?š‚qpk€yQdoôŒ|ºº\k²[VÈ%?é2ŠVÉn~G#2dÃìI]ZhVÿ´×‡=èJÁÁ,šj ÝÝÂѾ®-‹ÄÛÁ´s8ËnÔF̆aÆ™¦_½Ægd«UZÝβ[°Øß%ä—WÈJ9ÃnïÞ0 1t´7Ô{¨Õ^‚(‘ãÃ#n¿‹"{£gäÓ5èÐåÚ »‘vó>‘½áÚqÀohfﺽ>l®X<ÃYV„aËÃ# 1ô‰×Ö4J¼ÌQ‚£ÛYv‹,†C%¿º²t6òœe7" qžXd¥õ{2Œ2©[N>„Ô‚PÎ$ôà"˃½¡Þ¼V F:«:óÁXOrœo¦Ÿ#Îô+$©˜¼ÑKßµ¦ûÐåÚ »5ŽÝ>ÿâǪ±C%<Õ^{¹ïÕ5„ Ïœñx{}ØV04@gBâq7+F+¢¹ÄBk`°XP;˜vVlv£u`¿âÒA%Óßnæ²ÞO¾÷f÷žâ ¦J}'ŒDCTu*!ƒ õ»ðçøSëMaÞPo[«sÌ|ìƒ^çÖâ@ú¹DN~Þè¯v :t¹nƒÝ°ð`0¤F‹Í¬ýð¢*¶3˜[cÒÔÚëözý‰ÎLK$7êTªuäçÓ¦7›ÝD½–¯nkØ ÉÈK}3í7)±›H-[#(?ëé õ¶µZE†°âGf¿É+¹3X[ì‡îG7yz£g¼Ú5èÐåÚ»M÷ÌJWÔÌ»¡£TY â¬ý°²JÀ˜èVÇ1¯‡C—=ÓÑ^ö *ññŽÖ¦C›ÝU1hÕÅ +º±„ãÆgDg£ÛYvc]Šª+š–|’¦ní<;…zC½a­f☆¬Ú„AnÝgDÐHèzztø…à ä½Ñ3^ítèrí‡ÝZæÝ0Ñ`-„ÑH„á•™Š¢]î(uÛ¯65 +LŒ”ÛëÆzÀ(×&Aí`ÚÙ¶ÙMlÅD#w~u‹›kDdÙÛYvc3.POòi~'{NNð4ŸJR/9½¡Þ°VÓ;¥÷«biÈñè³Ò0ˆD…LyD¸øžotOÞèM߸ŽÏ¡ËuìvôÅ¡ÆÂqø —³ÿõÿaIÒaüÈYʸ¹èÈñЏÍsšt{}ØP¤Å¡»«åy9 œFtt·ƒign–Ý0J3ï¦u5lvƒÅà,*ž‘ï²Ô¦˜öÛ±¨c™”}@ÇìÀ›öü9!¾Fá~üâ§<Âëìw͆zC½a­ÖàK·®0+/Ó~7¤Ï>#Ön#CoôŒW»º\ë°m–‰°ÒŽ6¦L@Y-Qc™Dýj9´°íúË÷´zlìKÉ< ˆ”š6"ôRâíõaC= …²¥í‡ë0ã€cКŽv0íÜÚìF©É…Ø OŒÕÙ=gÇnZ‰CºvÌÍïM3A @ä /G"´ »¤³¡ÞPï¡V§ Ìλ¡Id±aœ>>r{£7zÝj·‡.×:ì¦VL ‰ ¿ÞQÃnaßР‚èÄr©³ÏRu€y–¨=Â)L¯^)·×‡è'4'¦] W1Ïr‹{ý¿v0í<ÛìFÝ£Qoƒ¢ƒ}ÎŒìÕC}³‡o³ì†EIŠŽ“¹vVåB©P¡F8¸å)rHNì’ΆzC½ƒZý úÀÈñ™×äFõŸ_í}›„ôðFïã›Öuº\ë°:„&\Z7b3] »é»¬è‹—ú]犰À”Ä9©0Á—]R#´×‡Íõ€N䋇é¶t"_,¸‡£L;W6»i¾†¾UTîØCÙÑ|ŒÚ;ËnÃò¤§`"FdŒ—™>³k툵(*Ô÷¿ÿ_¹ÅÍâ‡èi—×õ†zóZMÙ±´'”OÃmđ{wÞèsépèr­Énôÿø`¢]šÎ(i‰vËV<ú·H~¹?&;掆h¯›ëŠ€Ú”mh<´qn™}pnI™äÛÁÌ$šxÕ°5ÊIK.UThwãØ Þ¤òSFþ蘑 –‡0­Vþ#NÔÉ2œ7[ÿËiäCÈÃRÚ û‚Ík5¹=”ö áà6›ÕÐëx÷†qqP>ÃËÙs ¼ÑËçÓß÷ÐåZª>ÛZBc7êRé*Qþ5ìfYã#BMózqim¯²h¯;Ñ”T‡#X P‹ÇiÓÎ’]oYJDU¯ "Sí…â|öj£êÎŽÝHŸÄ±iP»Ð¥§}UFGBFQégžµ­ÙÅOC½¡ÞI­f*MÛ.Â\gÝÐ9Aƒ¾ÒK ŽŠ´èöF/¾heǡ˵6»iQýä×P{`7zÔ¡-¼zÍ'è"£Žµ×‡=èiÍ6âÖ@Ø(µSP;˜vÆlv£ZjQ Š·ÆkRzüF+e©öβ›öÍi-ý5òÌ0-ÜЂ‹óÒ o¨7¯Õ©EÃ/H2^΢Ä,âÀM÷Cr 3˜/<×’es²¹§w­p-àšìµAz¸zêÎj‰ÍÙM£?æ ©ítà©ùÔscª½>l®NUŽuÃ6X{÷ºkýlÓÎÞ,»Áï¢*'dÉO{àFüYvÃä«Q˜=.xV„a¿š™‹!Ç < m¸ñ†zóZ­SÎ4¹fßÞ¿|04yV £Õ‡ÅØ…?oô ¯u÷>t¹Öd7¿U%†ŒÛ-“çƒmŸÄn¨ïz¾¹Ox k %åÂac üƒ¼×,»©Çû`8:ÁkÑ'Û%“ç,»…µ¾ƒ¡,)¯ÍtÈb8ê“…ROR¹Çǽ¡ÞC­Ö÷Œ°½h¢º‹?u0p‹ì†PÔ4NôñF/¾heǡ˵»©ÓKÕ2TA)hó±ò¥bÓ̓QkôýìFÑ^ö hþØ`1Ë`É™5Æúµ¸v0í¼ÙìÆPµúh¶ô—ÛèSª´õì&¤˜Ã\Û½»v*å¬_;²]öQ¨7Ô{¨Õ x©ÏZYý|YõŒp‹•†Kè.”Øé&÷ϽÜ;×ð;t¹Öa7ÞÂÞj{YIQlÎnÔ!Z„LRdŽ‹=ílõj¯›ë x㎙tðÌ–×Õ³L;{6»!èøJ•¶’Ý”~ÊV/yfæ—ü ™QÑš 2"_ä õæµzŠ7<)¸i¢†És›=½Ñ›f~ŸC—kvC‡Ð$i¹öâÿ¬®¨a7Dlcy&ö ¨M“³Øåzqè ï0=@㯽>l®NæÜ¸÷áËÏ_$;£ì‹µƒigÉf7ÕÛÀnZ…#7:°^¶ÆFÏËdŒöi–Íe Bj*ÔÙfn±6ÔêÍku ˆ2ŒO°Ýr• ôgõî¼Ñ+eÞÛÿÐåZ“Ý‚–8Î8’+j†è¨a7Vh«L=LÄ}ªê¡%þ×81HRùìíH$Ý^ö PãtEÂ6Ÿ÷oq =«u_ƒiÅ3í`Ú/©a7­†‚ª¨½ÔFjfXj²»‘&²ò4m¹3&=·ÓßÀhl°>Ôê=Ôê,§¾Ü$LÕ>š¾šÃ$NôðF/¾heǡ˵»¡°ZÓ¥ø…ŒÒ¹ŒÈeSG »¡%Hœ>0+I=ðœéš*E˜S>è©"H½°\-»/©½>ìA°j‚Íp;ØÊ#°M8^þp;˜ö;kØM«¡Än¬,¢ŠÆ½oÓêšúðˆývÍG³£ 1ã\Oô‰Ð@émFˆÖ8¼¡ÞC­Îâe7-£Úk-4@¶½+Moô²9_ÁóÐåZÝÔêi˜4äÈqёꄑ»†ÝX{¢1ÐÐÁïÞèx¥¤O>£JÁ”úÛùduúŠöú°=À˜WF³E¿S j|ÚÁ´ß²-»ÁM`Ëe0Ú(ÿÄä)-A5ÞzC½“Z=E)ËnÑ\ »1TÙá9}\>Þè•Þëíèr­ÉnŒ€tú͉Ԓ=Ý#FKok؆Cš:U þâ³”èIjX†Ò£CN…Ç'}øé+ÚëÃôÂJ¦2gN䛂°”O;˜vNêÙzˆÄ—»Ùyˇ»´°K`ý˜ ʧPðõ†zµ:[ô,»Ñ…@ÜÌGЕ¥™¯ðelÞ6÷ô®®\‡Ý˜£=R[N׫×,;Œfoªa7Æn ©èÖª'̯, KA‡N vÎwo˜tÃR‡JÄ,IY8ÆvúŠöú°=@éªNä›–QŸv0íìØì¦^“æÝÂbŇGö@Qo—²LÚyˆj2ˆæÃA%ä'Ì.ôç õjuª,»¥¯PPp&û¸<½Ñ3^ítèr­ÃnÒ¡ª<çµHp†£†ÝOÑ•…qâÑ ,n\ðœ"ƃd>üê{gÃoi_@{}؃ª8‘ϵe‘x;˜vmvƒÅ2FiÕÏØ Ã]ÇeÔ[‚fçÝì¼MCe# ˜ŒÉ i †7Ô{¨ÕÙâG#ä4ËV‡_< !*<#üD[ö_ÁôF¯øbç€C—kMv‹_Àa=õªR±ÔX&Ãf´woø û†•~2¬-"yѦ ²÷uBy¥ÄÛëÆz  *ÇÞËíRKòbÊ9ÚÁ´_WY Þ—´øªÊ¢¹6*y¸^¼V>ØE¬ õ†zµ:ÅBü•úŒÜÒ!5=ôFo”·Õn]®MØ š[–ÝÐøX6©QÆ™’–©hznd›¥àA½˜fºöú°­@a‚$’ ‡eg.hu*í`Ú¯Ú»½{æŽñ2-¯\B(̵±á…‘ˆæà–ê~xC½m­žVƒ "^½žúGŸ‹ŒóÞèÅ\­ì8t¹Öa7Þ‚¶TW“&Rž•Š¥fì»Á8©‘œ:`ÛÉ/ª$¤¬®2u8ΞÊo¯ÛêM·1P¥ï †–)DvbKEnÓÎIe%4hFÐìØ b‚ÎBs` ¦£{†>qçf’ÿ`. ±ýJžêÂñ›Ä¹Þé õ¶µú9.aU >ðWýŸmœ÷F¯>ŸËÆÞèe_º‚ç¡Ëµ»Ñ¿ V”¡_¨mØÙmtzÓ v£C“ÿ(mzÅTà÷m.êy´ÿ`®—ž)%Ù^ö »iÞbj®§T^Wÿv0íìmËn"µlGA´Ž43M…¦ÔÙí/þ¬FEd´„”ÔQIEЗ`쌕†Ùgº s1‰{WÔl%YÁóÐåZÝhž Ö8Ôˆ¥J•£6q\MÕEk©$Í_ׂk&©BèБ~ÿŽÓyªF½j¯{`7FÑ#+¥Qd¿ v0í¼mËnvÞÒPÌ ñF—½ê/}¼Æí õj5 å#4tŽYÖ"Ái¢¡+®ël1.Q¡RóF¯F‚q]®uØ·h+7‡¾Ê1úMk#w »Ñp¨Š:Œ‹ ¬Ý@ vk5ñÄ9“¨«/ø©½>ìA¤„錓ÇÒ˜‹»ÛÁ´³´'v;MÙÓà»™øÕÁÞPoZ«Ã’~ ˆNˆ'Ÿ«›Êúœ)B²Ó©#ÏrÓÓãÙld=îÞ4“ëøº\ë°[`ŸÁŠRú1Zz[ÃnLßÇ|^Á´»6©-UÂÇãŒE$Î+Œ”ÛëæzàcÉX¼¦¾ýµ°Šo£¿v0íŒïƒÝN“A²?hÁC)Û,8 â8ÇÐŽ\J$ëï õ†µF Ë¢Þ¿ÅØÈ,ìå|÷†6=Õ,òDóD㼨ˆJñ½Ñ+½×ÛÿÐåZ‡ÝÐ!×†á•Æ>üj·ø.¥³‘»†Ýø: š!´ô/?;±Ûrÿ"_ÒÄŽ¡Št'ìvÚW5Hâ×O¯/ÛܼרMµ9Yæ>jC#Š_0Dc÷/à .fU„Œñ$©ØFue%š1ƒŒÌíÞÕȧkС˵&»Ñq¢*†Õû/^R!qǵ”#FKog«.•݂£½S ‘Ȃ׮O²ª4±TØæ öú°a/7â’Lë 2¸æw–E5¾Èv´ƒi§¿9»1“KsˆZTz8.ašf1µ]õg³3GÓg}¼¡Þ°VÓf#hѽ‚›…Eh±›5ex£W™ÏÅ£º\k²ã5h "M›ö‚ì†LiéêýR{1)Ǻ=›x{}ØP¨tñÄ$Å‹Èðœ-ûâÚÁ´³´9»É„±Õ­±…-,ß=wÞ8œ¹Ä[»¤³¡ÞPo^«§\1›\šòðFošùu|]®5ÙMc7vÃ>ýýLà¹ý°Ï"8ݴׇÍõ€š0ÊvcÐ*²3NH’l;˜vÞ6g7²C±R’âÆl¡vcFÌ…餅þ¼¡Þ¼V/‚Sg·E`\'‘Û`7íˆ;Sp Ðë`8zK»–؃`VT–^†Øa—9é0B`ÁÛv0íÌì„ÝDpd‡}Ëw‘E˜3âN#· Xê õj5]v =á°’‡NB=>ŠÙÙíRÄ6Œ¿»ñ# UÉ\--”_ÜX½ÔNq—®šy7Ω±ÇV?7eì‡y»–؃Àhép¾‚‹C~¸eSn3›lôÜ»aTç’E“DÆŸ¨‡ß/ßKFFä‹‚¼¡ÞC­Æ|DÿA“þ8¸(õE(uv»®m#¯Ãn0óVYþBmreƒäYÃnŒÝM–4ÚþìÊF?ØÛµÄô€öUIÓ†ßäk8~ÐMSnsšfê³v#?S0šÈ®XH3¬È˜&¸j"ž5n½¡ÞC­Ö6mºÖìPØ$ÔÙ- Ë>=Wc7ƒ¿ì vc.zËž¦!°§1a×E¬{»–؃IéO™yœß¹H"í`Ú¯Û»i®-˜K£½ùâ´SC6ŠEwjxC½‡ZM?1²úlÑa×Qhg· {¾½ v£áp"Üg?üÿÙ{›WO’óηj'êÿD-Ff7p7Þ¯ µñBöF˜;`fU\ A{á‹eîàöàM#]ƒ±i/ìîÑX.AƒÝ¢ÐX¢%ù”Keª}î'ò9'*OfÆ“‘™ñšù’߉ŒŒŒ—o<ñ|3"žˆø± òÀn|Ö2ìPùãZ¢= ã6ïÆ{»S—ê솦å˹6\8†‰Ðà7Ô‹fXr-°^ôéÓãr;ñá} Rý0G{îŒÝö Vés°C ´zÔ‚˜Rûe›U@=®%ZЀÉQ¹(RÖDt ¦žíúìöÊ–ßâ ·~À$ó¢Zýz¹ ž¼»z{\nõ$Zj=‡1O© æ£ç!s£7O±ŒO×å:»¡„gyy*]Üèä*Ç塺q]ºÀ÷Þ¤â¹výÿq0õ4ª³T1¨.ù¼Ûü0pò;hØ[ú±³÷˜õ’®>Í uu©^E`SáÅQb´Íü/7zóËøt]®s°[™ŠŽLå¸ö|€¹#7zóËøt].c·äBr\ZÓt嘃KTL„ÇÁÔSi¡ïÆîß~h}õ ™”èE9ú47Ô­Iµ‚µXkc‰[jGΞ’Û¬º.—±[rÙ8.-èƾܭµÿŽƒ©— vcØAÍ¡6o_2&¦Ÿ’š Ò‹ó47Ô-Hu > ìf'àä– v‡±[rTËCe=ðö%š–·ÒPΫ´”›ª9¦^¿-°›L¥1‰æl}‡ 4ó¼c2h1žgn¨+Ku¨Øa,±ý=vN§vŸ»%¯›ãZ¢®€Ô¼š•“°P¼¬ìNTL„ÇÁÔSiÝä¬C1ôUŽeÙ1¤—}ò47Ôu¥zRؘ[áOB22OÕ(o2•.UR)ÿ¨ër¥ª‘|ZÂõ ºú;.uõV hZ Ƙt•KÓ¦Oá[zÉÚ8¦žÛ|r‹©IŒU‰œïÆÊ5ŸO6%ðî±cÇdÐøõUwn¨ëJ5#í~%…‡“`ïž;¾ÿõGó èøÌ­”y÷RnôÞ¥TÖÕu¹ŒÝ’ Ëqy¨«>*Ȇ¸iÎú”zr }„ÇÁôQ-:ª³›,ßf6•n0ì&·˜Uñ¤^â'ƒ”xærC]Wª-”eÙ…üÂSHõïÃSF-ŒÙ÷¤£í2öž8r£7N«¤»ër»%•ãòPW tÜ<»9=}ŠqZ<ƒ©ç§:»‘=×5Æþüés4'—®r7MéeŸ<Í u]©&u€•<m±ùŸ€0¾%¼[ óäÙǽ})„hì6F©e÷™ØæÉH“ï A¸#œ°Ž¨ñw\KÔÕ‚ã“~áËQª÷X¾û޽÷Éøÿ8˜zæZ`7r(fçü²uIüm?Jˆ½^ÒÕ§¹¡®+Ռˇ#Bau×?$˜h“`È?!á8!Dc·UYj$ÀiØÆÉVg8¸Ü¸zqŽ;®%êê¹d¢uåé!Ý^}þîAŸã`êhÝ&‹/<ÚzÎÇOeÃɱÏwn¨ëJ5ìÆà$“¡0¶Rnø‹/0ÔQ€rÚãÍ  ÈðßT ”-ÎßÊÞ<Å2>]—ëì&#|ôúƒ qˆ·8 œO6ŽËC]=  “D‹*ñÏsçØ§2»_°ìBÖ_ŒóçNR/¹¡®+ÕÒwCˆ¡TÌ'„ï¯Ák~#T!¹Ñ ¥›Û¿ërƒÝà²E{]d˜G‹ßZù¤â¸<ÔÕ 2I´¨ÿüÑq0çqŽ}ê²ceI_$©—ÜPוjwŒÈÓç íOX‹Á¢vã y•×äÝÜè-æ°€g×å:»¨åø$ŽËC]= ”4‰UâŸ?:æ<αO]vCsòéå_pë,L¶/¾HR/¹¡®+ÕØ£ÞÜþbÚeS·)ø«ýÓXTîÜî•…©çÜè-䤈W×å2vK.#Çå¡®PI¢E•øçŽƒ9sìS—Ý$'c#9{U<Îäª;I½ä†º®T³æ‚y·X¸É]ŒR2m¡`ËSg«#ÃÅü¾yÁëÌ;3y7+7zóËøt]®3±[¾-ø6 Òqy¨«”Â&Ñ¢JüóGÇÁœÇ9öiÝX|A6$Wû_$©—ÜPוêoðç°ö!Ðg+¢¯w{úœWüv=8d½†ßžk,H¹Ñ§UÒÝu¹ÎÁn¹·àÛ$NÇå¡®P ›D‹*ñÏsçØ§vc@ÒOî<\|1ΩæNR/¹¡®+ÕtܘwcvFÃÁt¼ +°²Õ*†jb…‚ƒ[ ²ÕÜ b­=:»åÞ‚oS­×uõ€RX§EË.ë>¦RµÀn÷9†ËŒÍÿ³áIü*¹P4¹¡®+ÕŸ}ölq†‘?ìF¯M¶Ø‚ãBhàOH?õÆëÒÑãóÃ{ŽßÍÞ8­’î®Ëuvc²˜Z`þ"þ<¦|r\êêAf²ËÃÕªÏù¢£ v»Aë²Z“3·ÆjÉha1ó„ö‰r§öïãr«DΣºRME3ÌÈ·Ý7¾p3¬/†…Î\˜áï>úް[çÜè騿{Úu¹ÎÁnR¹ãÙyVk"TM¾zÅ|\êê×äý 8a…ðÙäL=¹ØÍu(ä$hŽ~òŒÃV”9ž>ÿòþhòtr›[P'É»íº\gb·q#ÉHïØ§˜û¸<Ôe·T‹°’~L=ÕÙM–IØMŒñ ¬ùŠcžâï:kôò†½¤Ä–ÏØ-’ÝÅ 4ÌèóEÁà$—[ þåT5ñŒ».×™ØÍÏAŒ+[¾xÇ>¹ÝÇå¡.» >ãaÞ}‹°’à|L=ÕÙ  C1ˉIìv7¢>[ïÆ_k>ù•Žž^Àø§¹¡nAªãÑ $•BÕ¸k7fêMy=7zJÒYu]®3±rè&åYw9ú YðŽ‚$v—‡ôÀñEXI`=¦žêìFöd~éeóy8«<=Ïþ©Í»Ñq;ÒwóHÎ|H0ׯ§Âɨ&„4ÎCŠOnA ¥›Û¿ërŒÝøÊB&ÇÉ;¯?áV|r‹ÿ¸<´ÀnÇay@Ž8Žƒ©§Þ»‘CLÓeiêT,ôlËSè¦1‘ø0¹¡nAª}aWãc¾:äYÓÿX˜ðýÆ5®£i í÷ÇåVO³©Ös8~ÊÔ'5vëk±çãÆaÆîÜèÓ*éîº\gb·»™ß7/°©F2ùfbc§ÅÕ—ù$ä¸<´ ðüzE·Àn@-f玃[%Ïw“Aƒm !W©P‰jòè¸ÜN"œÜ¶ Õ“,é· ³î›ïd»üé–¹ÑÓ³šïi×å:»ÑØïúkÖ;ÒãðýŽ|0‰ù¸<´ óü“Ú™Ü6Ân@ÍgŸd:»¡l£@ª¹dÂn¾|`RÆÈÛãr«'Ô‚Të9œ¸ô˜ƒ©Çß»Mr¿Þ!»Éë»osC]WªÙÕab;-cÂʶE$ÝaiÓóLìÆw¬àÕó˜òUÇq-QW2nöçnÊÒ;&#9l•ºyÌÇÁœÇ9öiÝNs¦üع»®T»m<ÿàËãžšìA­,a›c·EXÚô<»ùôÕó˜òUÇq…\W2|*0ó.ó;8ÄgwKãõŠ®ËnL³"Å˰¹Â‡ŽÝ Zâ(o̼Ëí,Êu¥šé{¤—™ hK¶íøMÏ|IŒÝ<í;ÎÄn2yS¾ª9®%êêA†‘&€˜Ùaì ‡ÌÁá0v“½ c~W­JœÄ;ô €Œƒ‰EáD?cçãì¦Þ¾¼_ãvƒ œˆ¢&?‹oÅ{—[=­ºRíg6}gmÙÄZ/ƒÙL®áÓÔó3±óD²> -Á´Q-œk‰ºz@pã+×/_e¼—[ü¡<ïYÞã`êù¬ÛwCb) ´s¦<óq²Ö›º€Å~7„ˆ¢>>IšêúRýö%c;à&šé²1}:lÏJ‹s¹Ñ›f¦Ô}×å:» î¾fÝÅÓ•þŽËC}=0¬ðÝJ& Öú–A÷8˜z>ë²›ä vû“?þ#qëgÊCa²¹œ£¹á^Ûd…¢ ‘êºRMo—._@';ðaàÎ ÿÑ)öótŸÈ^(xnôBéæöïº\gb7>Æ`„P®Íßf‰å¸<ÔÕƒl²Ç8=ti"x¶EsL=½Ømß™òÇ;kdrC]WªÝF‘Ožù/¶}`xg ¬é¦%à ¿ÌÓ :Ä™T1[Çí±ñmnôÆi•tw]®3±JX¾iù ÅÓ•ŸÖqy¨«¤ 47 $G7ŽÔ‚/fÇq0õ|¶Ànã2´îwåò‡q€|îÜPוêù :Ÿˆ÷|E<ëÉž$8P#òÍlì–Oö2Å|v[&t2ægX>Èb<3á¦D{\KÔÕ¾h7·¿xñ³sáðž…ÇÁÔ3Ü»ùÜ"º!ó&­#7Ôu¥ÃTt“ñvÅØ7|Ã}›ÎçFo’Éb·]—ë4ì&tÆç®[Øò×ÃØßZÆnÓ>¦x"ÛÅi†yõò¶ÌnzgA/׎§¹õX]v£G žØ“ø•ôËâWHWNA57zJÒYu]®Ó°U̸:ÒËIñôÝÜ1XK;e•‰ü¸<ÔÕRŠÓ óê5nìæñ9.·>ªEGu©vÕr.Þð+´Å¬Î=Ýdz:õœ½y–Êøt]®3±›û<VÑaã˜}+#“TŽËCu=0æuspK3“‚ç¸=¦ž+c7On¨+Kõ› G É–È5Køu].c·ä"r\*ë0"=v„Ã¥qOÚc·›a(—7¿±†»Éÿw\nõ<Ö•jæÝXïFùsƒG`,~ªÁhn“·/±Lc4ÒmWòúæÝX®hSéR%•òº.Wªɧ%â—h-V} ¹®X„Q<{S)òÉ-†»«gHÞØùÿ~ÏÿÛÕá2½8GžæÖcu¥š}ªá, +6|øÊ—¾*Soy Bú8îÜê0fnô|ö ;º.—±[ri9.uõ€ˆ±Ûdu¼~«³ú²í€ÛÎwØýÛoX­ÔB¦GÇåVÏX]©f3OŒCürï˜ç[kÿÔ;dzØûäFÏ'TØÑu¹NÏnf39ÖÀìëÚ bßÕŒ¹ÎÚ°¸Xú®› öôâyš[Õe7>0ïçòoÅŠihå/7zJÒYu]®S³›[ƒIí,žÇ”O$ŽËC]=  c}·ñ§Âª[ï»Ñ/ G0é)¸>‚±ÛßÿÝ*¶ã1ßlÌ»ñÙ¤ßävº›éæmäx«ŸÇÙ‚O×å:» Ç ŠH ‡™Î¨"ÇåÁØÍWÜq0}T‹ŽŠ}·Åüؼۘ¶"Ý‘ì&HÔ0×vÃȰþ!Á¸1}=gUrë–uf–‹•æªEGkì†Í¹ï\,f8Ÿçq¹ÕóVWªI.“ãläu¡³&(€:!Y\@*cnôBéæöïº\'`79M^ÍpóÅ%Ûì袛O*ŽËC]=°ˆŒ´kt/[=,ÈäyL=cÍ°Û rËÕõ™ò:Ôu¥š‘INØt¨ f– fbŽÂ77j»(ëÜÞÓ°B‹ìÑk{·ÇÇnÛ)WÁû¸B®«î@ ó2’ã‡ye÷ÎbÀSÏjìöæ½ éСpWã/7Ôu¥šaI¸½"ݲn÷ç†wÔõò2%*Ï$©.ù¹Ñ“<—ÿíº\'`7¤NOòÛõüE]= -5Ków×üºBhÝX2̇ö$¨P ¯ç9ÓÓÜz¬®TÓƒË0I/ÊÖÍ 7᜽M™I¸ër€Ý«R ­åö<.õ§»ÅÅÃ6D óbGÝû0¯^ÝÕÙM f±äS¾Êüô±žù´OË­žŸŠRMÆä y¾0šºí¿^œÉÓÜèM’+vÛu¹ÎÈnwç1!ÉÅd`œÐqy¨¨ÜN³çæWÍÜ]‘Ýï‹t÷öx²ššg5·Ïq¹ÕsXQªÉón\ÃIm¢n^}þiÂnrnôtló=íº\çb· ç1µ,õÀù†yõŠ®Ènb‰Ç,¶y¨Y˜Ž ·zž3=Í­Ç*J5ˆAm`Ëü&ãÀ\b6™Ðð,7z™*}5Ú®Ëu&vÛtÓjµîp\êêyÁ»ægìS‘Ýds':nã=¸l5wžÕÜÌiºý“Ÿ>gÍŽÿò?>3v·…E÷qm¶mÏ3±²S>xËCkìF‰úæÕ+º"»‘±±”¸™bpXÏs¦§ÇåVÏX]©Æf’? &é¸Éaim¥r£§c›ïi×å:»í9©ey¨«<2ìÒ [óaX‚{ÿ’ŽÜ«.»•Dr5­ÜPוj™w£¿ÌÇ8ìvÒn+—½ÕêË ër€ÝöÇ”Iˆö¸<ÔÕ‚ vnÖ°Á/±ÈZ(æã`†bÿÚìvC9TÞ8æêºRMÇy7·³Ö¯ÿœppd›L®ŠLn©XÍÀ‘'`·}ç1M÷¸<ÔÕR:TÓ2\†= jA/{ò§ÇÁÔ³T—ÝØå‰õ²EÉ=¼7…Cg³ÌNô̧}šêºRÍк̵ÉùÚ~Ó’TæF/U>·ÆÓu¹NÀnh`DwrÉ”ýÖªLþ¸<ÔÕ¨Y4t†1‰"3§\„U—ÝZFÉ€šo æƒèP@j¸é2˧EŒŒä¸Üê Õ•jLθÈ!âí/_Ì3~† ,r£ç³QØÑu¹NÀn¡ê~°l3(ƒÿqy¨¨æ•M·0-CPþ–L8†³ rRO%¢‹éÖe7²Ä·™Ÿ s9¢Ö[ÌmVÏÜPÇKõß~ü—‘§H°˜3äln>!Ø‹_9¤;$ÕLÒ€´¿V›@nô²V½y×åJ¥:òi‰ÑUj§ü£ãò¯6)¯‚Iš¼t"pøeA!=Þã`ê9Ì'· éŒô¶üÁtÿrC#Õl•ƒA#ìÃ…ìñË'Ö*Ù­J5¨Þ­¿xô˜ØøîåûÁù_U¹e2õ±omnôj F×å:»±ÃƒÛuéI 6%Xz’Ëï¸<Äè­¼&áWõÃ~JÂ;Ò® Ú„ûq0õäšb7=«¹Ÿæ†zUªÑEnTv8kfü Íé·*Õ¤æfŸjã­H=àËâ0w—tSÏ­±›Ç'7ÔºTC"žÚ˜‹äàwö‡ôž=f8Q‘ù©fhQæÝd®“´œðýyqÀn N’"TÈÆË¿üÉfœßæFoœVIw×å:»1¤ãúé7È­\ðÍ„ñ‡’Â@ZÇåA×J3_}£ Ã¥'wL=~c7On¨u©†M¤¿Æfþc1v7ôæ”î[ŒTóL`¹$N¥öx‡ôÝøB† ¹bFŒs£çóVØÑu¹ÎÁnRãŒ<øÁ4ðZŒX&—–ãò ëqÛßêŽÑ¡a^ÊÕã0¯^¿ÆnŸãrë£ZtèRdÂ8ŒÎEZ˜ˆmˆæÄ'FªI¯_†"ýE_ 7¥žçÖ—óô9½¶x;áÜèÍ3YƧër‰ÝÊT÷j*ÇåAס6ã£æÃ¼lZÒï0¯^_ÆnŸãrë£ZtèR ¯ÁbX*ÎÅXØ M5$>1R OÅQ³d€s¦ßÆ™})lnô!-àÙu¹ŒÝ’KÈqyÐõ@¨ÇøÇè“ óêõkìæñ9.·>ªE‡.ÕßþàυŘó’Œ ¥ì(Â#ï9wÄH5Q‰0v˜£ìqÎÂy`¤Žó'欗“½…\ñêº\ÆnÉeä¸<èz`Þºã}bô€Çc˜W¯_c7Ïq¹õQ-:t©fZÍÛønçh å1û¦yŒT3•Æ,¼LXHöØ£à!Ó½Ë5TË”œ,Iµ ¸ÉÉ»3Wnôf òèº\ÆnÉ¥ä¸<èz@iæ«bô@r@ŽDxL=uc7On¨W¥š™5!¸ìöä™NmÈ|ŒTÓsÁXpÉA±¾øc‡3qyú\Äá€WÅäFoœ½’î®Ëeì–\TŽËêXe±P€=#SOÝØÍã“êH©†Gèlj…$ÖŠ1‰ò©&u:_ŠŒ¯PwŒ{^üìÇXW€^›¬¥…㮣÷ôùØgâÎÞ$¹b·]—ËØ-¹œ—](äµúÈØmRÝÆnãrë£ZtèRdJ¯m¼W ”‰¿ÌÄ-Jx©ž.{XÈÜè=L­Ü]×å2vK.(ÇåA׋ <Ò³ŒHéq0õÌ»y|rC­Kµ¬æF>ç’,즘—´ Õ¹ÑóÕTØÑu¹ŒÝ’KËqyÐõÀ¼ùÇû´ 6~L=9c7On¨u©f—XŒUisa–K4Õü‘ø´ Õ¹ÑóÕTØÑu¹ŒÝ’KËqyÐõ@¨Çø· 6~L=9c7On¨u©–¾æ÷“HLM¬ïæë¨¼#·Td-‘±[rx˃®bX,ÆØmRÝÆnãrë£ZtèRí–rÖ’Œ&Öøm¸Ø´$$Òø· Õ¹Ñ[„´€g×å2vK.!ÇåA×J3_}Ô‚Øøq0õäŒÝ<>¹¡^•jo@"4÷î÷É3eÒÍØÍ×`Gn©È‘g§±›‡"•ã¸<¬êU 0v›Ô²±›ä¸Üú¨1RíÎÎæÄRjÎjÄÍêÚŒÝÑNå™[*Rås1c·EXŽx—‡=â/ÝߨmR³Ænãrë£Zt¬JµÌ¸ñ ÉÅŠ€É4Ü¢xç”ê›áüî壯ÅÌÞ8­’î®Ëeì–\TŽËêXlã1ž9õ@r ]„ÇÁÔ³eìæñÉ µ.Õ("fܸØdñWï,Rýú“ïýæš\d‰tÊ¢’#8Šð8˜£ÈœÆn”ÜPëRÍÓwm³Í¸x¤Èö6©~û’SÛîÏnó¥èxóÂ-CN`ódFGÉ€Ûj2ü—½pÊyŸt].c·äÂq\t= 4óÕGÛô@rh¶GxL=Mc7On¨u©f(’I7qÂÚÄ­Èv¬T¿yÁ>“tÄ䢇Hïìß~ò¡GÀ;¾÷­¯AmãÍIÜ„ ;ß-8D™=Ÿ·ÂŽ®Ë•ŠÝÝL¢K&ÑB«¿d€5¡1!s‡9ˆj&$‰ö‚`êÚ ÔÄÌÚäI#ä9ävj…¡xÄ›Ÿe›;”wcØ sݱ'ÏèˆÉ†“ü: –GḠm±)¿qq~þ«%$žŽ=Çî®Y`\‰»ërÔÃ(êÞr*Zâ§ÿüÓºÙ8Gêf±z¼Ô¸ÂP(¢Åé6? §¼Ãn,7 ÿ%Ûþûšå@9ã†È½'––³ÔnLyrÔ)Ç ŒƒÝ]³À¸ w×å2v›Ô¦Ý Q¹-T÷E ÖÙ§tŽ”ë »‡W+º$'ôõ^}þ©†yõúf®Ëeì¦Ôì•]Då¶PÅZg7‘§6eÞóüìÛAvÛTÑtè0&áò5²AGOùëšÎZ.c7¥f¯üè"*·…*¾Ô:»A^p™L„Asô•°QTmü(fdÒW4pÄü£ÿçÿøâóxÏÇÛ—Œëó/]YÈa¯®YÛØ­ˆŒô—ÈETn s¨W٠–ƒ^›ç8Yh6&²Ew<»ýï¿ûﮇx¿Jhë W1AqËžŠžwsÓg\2êˆÃϬùºðŽÿò?>s÷<¸8CçÃ[ßÍCÑŽÃØ­ºh*'Q¹-`~¨[`·ûêf»ÈàŽ‘÷an1&þäòž!‡±[™ŠþÆnÁo9鋨ܪà"P·Án7Œ7Êî^8†³ÛD€¥p³Õp7 Q.½÷2v»G¢¡ÿÆn UFKY¹ˆÊmò‹@Ý»¹ÍÿŸŠ´»)àÔzdìV ùÆÓ½ˆÊm¡.uuvƒ€`(¿Þ ·‹–ôݰÿ[•ÈEZŒÝpj=2v«…|ãé^Då¶P º:»Q×nºm8®”Kqp»(ß|ÿ®ñ#NdÂnì3q»MiáÖØ­…Zh0Q¹- ¨[`7ª›…l,^ãª}zm¿úõƒc¸!/eqñ»…À¬èoìVü–“¾ˆÊm¡ .u ìÆ¡6£•n7Ãí‚ ÙHÂr¿þ¹Ü ‚ðϤK}A».×™jD´;à°ë ûñ_²­yƘ×jƾ¼J9¥£vc4’šá).Ü.BÍŠ¶ÞbY–“üÒÜ8ÀC×,° Êu&vC? ‡v="°h¼§¨î5Ân²Þ%o N†ËÙ çïðá!¶%fUÒ¼‘á3±Åàb¾–-Ì*˜ÁˆÇjH pÓSm³:»Ñ½‚Îà)QÔXK¾ÿ?ÿEÜó_HMV|câ¾Ýº=*çÁ¼õÝ<í8NÆníÛ{NþìÏ>€Ýøví½ –ÿF¨Înœ_» +†}&ß¾ M¥¡™w7,Ièâ1JéfèÞ¾T4vSÀ©õÈØ­ò§‹`»5^G}e¯:»«Ø 8&Ѹà¬ÐÈ$é9~ ÆEx(,„¹±[™ŠþÆnÁo9i뻵\;=æ­vc4’‘I$¹äŒ›E$ɪŸžc@Rö¥äרm®f=Ýš­šº³¾[]üÏ—z ì6A5´„I7®Å?nq<Óún‹pÕõ4v«‹³©[ß­Ùªé4c-°³iÿðÑwŽx¿Yå4c·)" Ü»5P -fÁún-ÖJÏyªËnÿö“9 íw~ï,aÃ!—n(²ö«Wnïåù#c·9&Õ}ŒÝªWA›°¾[›õÒo®ê²›˜÷;û²=}î®'ÏX©½Oc·­ˆU oìVü–“¶¾[˵ÓcÞê²ö$t¯8“”í‘qs1éÙ½zóv˜Æn›àªØØ­.þͦn}·f«¦ÓŒÕe7 vÃÚ_Ü¿üÉa7(ožÆn›àªØØ­.þͦn}·f«¦ÓŒµÀnïøñwÿúoÀï}ëkŒOnÓØm+bûU¿å¤­ïÖríô˜·ØmŒëÝü®\cÝm6“:>M=5vkª:ÚÉŒõÝÚ©‹sä¤vÃx’mµ¸p„€eÐR6*™‘ÃHæâî”f3³¢¿±[Eð[NÚún-×NykÝØdÒ™MŠåä£Ç¡µoÌÍÉ^%þðnŽx›3ݸŒÝÆh4â6vk¤"Ɇß6yÌnròB#9´ltŠ@ ìÆ^‘ H²*³on'®€U ç»a΃͉Ûrù‹Ï@`yc7œZŒÝj!ß`ºrê­%ÀÙ.äSܧ?^³Á9S–ª³›Ÿ2cM·Ø–@X‹g}ûƒ?‡e³åÕ-—¥ŽŒÝ”Uc·+¥V–°ƒÑØÌ ðùŠ[ W­\Yºç@ :»‰¹ã«Ï?…Ýø„²cÈq/Ÿyœ& ›-¿ÛrÙúns¤Úö1vk»~J玆?Ù°}7ô>ܖΊ¥w.ª³pÂYröÍÿúû¿cÏÉЊØÍ}ãqhéý™nîl¸'Ï” ±¾›N­GÆnµo3]ä.Cyv¾ûÕ¯Ñf†-W½ л±ó?§‘ÂS¬€éüÚ· †,‹#ÌW¾ôÕßøêoúG¡S$€±›ª‡±[;uÑHN`7é¬ñKß_®‘¼Y6úE vƒƒø“]¹øÅ\dÏ›Û_°O—Îhãw‰ö”º´ër²FÆRgî­0&£‰%‰üšlÅÐÂÏhݾÿõGn™ÛëO¸p„vQfÐ’LfEX¦B Ö5 ÌJúΣër™âzW‘æ`Jv_Œ!pØÍ­t{ô{HF{ ù»õnC…,‚c‚¢B Ð5 (•Ûu¹ŒÝ”š½ì£1µ‰ åe¡°‚§B vC°™ncÒQG~C†"Ì»1=ÇÒ££÷óOÿ"D…‚O×, Tq×å2vSjö²Ä¤D8N–¼] +x*ª³Æÿ0ÔØ>êÅÏ~¼XºaJÎ=¡ ð =hÎØm«–=ÝZ®Zy“…oÂnµò`éž êìÆQn0}1¶‘„éø…³Af‡.†%ФsÇz·»Žž­w[«aOc·†+§fÖ„Úl™[Í:8WÚÕÙ 8Ýî[ÃÙܘús…ºc2ïöå?ü‘·™„¹U*¤ë¼³–ËØM©Ù+?B0 ¸ñ0ΕѰ²G v£Œ:Ê%kßËż<¸ø(äiìB¦¢¿±[EðOšåçв×-°g`âAóókÞGãšxr&ÎÄg|kì6F£·±[#1Îûógºi$f*½¯ß18æîÊìÆžZ¯?ÁØ}³ ëÝÜF[?±ªzý£ÿ òËÅL\hSâ0v `YÓÛØ­&ú´+ë@®ªxŸUiT³n¢u¥Rsk܆õn8Ä 2tdYuÓsrÉapáÅq‚êYµër»Õmò‹©×Õ‹YªåÙuãªZ›éÖ•jfÙ0}dO¹o¾ÿ.å|7¬J`C™žƒY8àvQ6›É6+œ+c·06ÕžÔÕÕŠ½”°±Û*]úµ ÕpÙø8 ÂÂrŽæÝÃÂp”ÙLÎQjßÇØ­Á:jA4‹±[#q<-H57Ïn K2ö¸X®¿úÑ?AjÀ’$Ä€“Ï*¨]—ËØm"¥-ܶ ZÀ]³€/ÅÜÑu¹ŒÝæZݧ®`†i.ù|w­ '»n\Õ©© Ô•jŠ9»±W L½Ñqãn„p>z9‰ð¬‚Úu¹ŒÝ&RÚÂm]=ÀBW>\ÍØÓçâà—%?UéºqUA¬ÙDëJµËœÝdÞãMi?K¹ça&žUP».—±ÛDJ[¸mDØÈd Âpš<4"Õs<çÌEÇ F-8Žð8¾û_¶y·9tû»5XAè¶-rã3Uÿºþt¬Š\s‰7"Õs\棎 _È\›ìd‚[®ù»Þ笂Úu¹ŒÝ¼|¶ãhD02C3¯ K׫.t­¥ÞˆT?€…Æ¥j×ÿí7gÔ®Ëeì¶]³¿Ñ¢È^èåºn\ËEºªo RÍY6Ãú5WtÙô%l›*ꬂÚu¹ŒÝ6Ép™À-è2%]M¥ëƵZºK¨(ÕH‘Ì 1wöÅç?ð ´õ©´MµsVAíº\Æn›d¸LàŠz LãSéºqÅó !+JõGúûtÓd~MÖ¹°:‡±ÛªàuÝÝVë·|€Šz |aõ»n\zÑ®ö´¢T³Ã~öôö!8Vhoª ³ j×å2vÛ$ÃeWÔe ŸJ×+¾˜WÙšTÏM%ÔÂYµër»éLﶦ23&Ú®WL¯¦5©vÇm?}ž ÿ³ j×å2vK%Þ ãiM¬&°f_€®×¾"Ÿõ­–¤Zvþ¿õ‡àÇü¬‚Úu¹ŒÝŽ vòšÑ7·,bÑÛí/ô2~ÿëØ’†€e;?°;Ÿ~rNÖäöá›]7®‡E¹ú]R}ÃÆ#²ù?Ûܪ²·©ÂÎ*¨]—ËØm“ — Ü„xý‰Ø•‰Ù`G,=¶g¢1~û·ÿÝÿá£ï°òèß~òá?üÿ·ÁxÏ{(¤Rä³>jAª1žäÄm¶ýç±òæjÂõò†Õ8´¿®Y@)X×å2vSj¶Ö£ô€#©§ÏÙ®„ {3nCh ÿÐhllÂExôF(0þvדgÄï~9y±ëÆ5)ËÅo[j„í»ý7Rõ£â–¡†åzyû’0èO.äŸ ¾Ç–Ÿ÷3¬ëhì׊þÕõ«4|xJ@à€E0nÉSza´}×ß~ü— ztÖ B¦<¸>û·[¶aWwݸ”r]ðQU©¾aR>Ã>ûìü_þä‡Nªß¼×’Œ”òéÅwò)—û{ôŽó»Œ_<« v].c·±ˆ6⮪4y²Ÿ•`NÍéÐWîí­¨×ü׺cb†M„÷Pß(ßÄéºqÝ—Ñþ;*J5ŒæÀß¾d䜾˜fý ón¿ñÕß\¬›ÿøŸÿŒ¡ËÉ&«|ŒÉ8¾ù[gÔ®Ëeì6Ôê>õ€/;tv§noÝñáÁFéè¡@hüÑ•£kæã™;ˆŠ˜åPÔË,2‡åô>¥Z!¿ò¥¯º‘ð'Ïpܹ£â;l)‘üSêÒ®ËuÊé]QTÔ"-±÷?ü‚²‰:ª€|37!—³F ÿñìG{ùqÄð_×+\¬+>©(Õˆ¨L W'ˆ7UÒYµër»m’á2+ê_@‚\ø0;¦ÿ¡4üGnƒ£pGoIè½®פ°¿mAª'U  öÿþ»ÿþóOÿbòJèö¬‚Úu¹ŒÝBâZÑ¿=ÀŒ˜ŒêScc ‰UÉdÂb`ì–À‹sô>X×Ë—Â ЈTGÕÅÛ—LÌ}û¿Åñô²äÓÏA‡^?« v].c·¸VôoA°$1Ã;CL"?úŽ“|åòë^ÁèZ™Ü¸ëÆ¥ vÁGu¥©ã»káSêÍ a?9®HÍMÌ Â,“t ¼lfâ>« v].c·‰”¶p[W4gÌÆdp·¸ÿÇhý7„a|R_F´)0ÉuݸẬg]©F2‘RD”áY·[aÞé“!JnøßûÆ7ßÿ@¦êÜx;gÃ…wœ;« v].c·µM]= ÿÓ–ýR Y €1ÿ"Vbäïö¤åfëdùÀÒ2"^ߘð]7®E¸.ëY]ªá)Ì™à²w×ÓçÐVè³y–úÒ¥úÄ‚Úu4vkPÛT×Bg~÷-Y÷ -b%Tȼû¸ýü K²\h1$ž›¾ëÆášþÕ¥Z`—~™ôÈB¼&!é¸yv££§ÛJUP».—±[ƒª¦= »oÉvþÜ*@Agò=,ˈXΖ*p×Ká‚ZêM°Ãƒ~Ä —Õ1¡Î*¨]—ËØ-$®ý[дk·mÆÁá›ù",|ËÇ0¿«ËˆèÆîºq-buYϤzøqÿ7êåÝ “÷ÞïþŸUP».—±Û;mÆÕ„rÀ¬âNÉyýÉ0˜yÃ[ –WNE¸¿_ÿ‹ÞÑ#ˆ‰¨fGšê-xapÂxûä Þÿ×ßÿÝē۳ j×å2Õ1Ôê>-èš6s ²$ · ,£—„áÂá'ì_azŽ0ZÌq9]7®Åâ_Ö³©Þ>‚Êu4€%pƒèúSƱUP».—±ÛXDq· ¤i»‘ɵ‘ ª"c’\2aB’ÆB̪I¯pu³ëÆášþ-Hõ&äiÕÏK8™ƒvrþèñâgÔ®Ëeì¶IàËnAÐ cif˜tÔ:Tv1ƒ|ñ³Ký¸ LgPìUø]Ô>­®—/…9@ ©ÞTÈž„ÿöî>öž<“vFÀ&+6v«~(éêz`Ó95Ðmßjs·2(|\ÇŽÜu #z…Æn!!éοºToEŒAH¾¾™D¼á5™MæKodaò.ʳ j×å2v{' Í¸ªëGX[Ω¡ù;ãÁÆŒiwÞ a)}·û$oVËéºq…@¸¦u©Þ »ô×™Ô­¤$Ú³ j×å2vÛ*ó· ˜ã{U.™zù—í(cŽËq·—Óuã !vMÿ¤zò2•ùÊYµër»EJoÉ`­éºrÌÁ)@.Âè+ãÀ—püq9]7.± >jMªW«€)9Î{ØxƒÉ»‹)ãÉ–ËÏYµër»­ yù-è˜Ã­X­FcŸás³äù  Ã…Cöéºq=(óåoZêM•ÀÈ$Sor1ðÎ…O[° ÆŠÝ*‚Jº²ˆ>ÜŠe­4v9Îÿºy7õ¸mÙ‘’0ζdílc·tç_Yª·ã%æOŒ30pádõéswÐÀ“g‹V¾gÔ®Ëeì¶]곿QWÄnžÊ21'T3IǦ¬5`)7ßÀNo\ò`‘ì2Ô^u¥zªï¦± €["ò¼ç8ήY`\‰»ër»Mj³…Ûºz€‰3F 7nCa…"ÐÁ_Þ=SÖàOü|ëgë¬ëÆ5/þ•}êJõä‘jÎä•qp‹›¦±xXÆYµër»íûܯ´ â·’“ÝXt7óþ惓!ˆ$0kâ„ݘ¹CiÀq¡ð]7®P¡®éß‚ToB^vÝaj˜åÜ.%OŸ+¯ŸUP».—±›"±µµ 6nåNÀ>fÚùÊ ¡G` ÆhÏGúû×T!pNìß‚To‚—A „Sf‡ù½¦ù“±Û&™±À«´ hÚÞ¶õp+;™‚WÊH´nÝ“g°1/Îbø×»n\¾æ¤zkE ØýflÚu¹¬ï¦ m•§Íè·ý—[õ³áOÿÊ•ˆP4œË* hž%h3Rè –ÀìÄۤ[ûæ–¼ÿºf`©:Ÿø6vSj¶Ö£ôÀøPõCmd.‹½¶P© ;«ÒH…OGñ´ Õ›à‚Ñî†%‡ñÉï}ëkÊëgÔ®Ëeì¦Hl­G‘z€ÕÐÌ[Ñè¸Üâè¾Ã4ýÂV$²P|¯ºñçÏÙ ]¦×•¡?oHæ­"ƒáß¾¸Ò} ;‡z.j×+ˆÀ%DJµ.À‹Oã¥:x±wÂúWÌŸp0›Œg(†³ j×å2v ‰kEÿU=€-½3âNWô¿P 4·Øü½g¤½Ž1e”¦í¦×=†¶&˜@©|ßúCmîŽ:ýè;ž„ß~û¿Ex™½”É3×øéÄÝuãš”åâ·«Rí¥t«#Rª7áï?Òh25¼Ø|œgÔ®Ëeìæå³ǪxGmrº(#'÷L§\¤¦`ëvûÎTòÉ3zmÒËsÃ8÷6f ˜ÄIn±ÉĪD²­îºq)åºà£U©ÞJj>|¤Toœa„“}uøƒÝØ[’[¤1ÉYµër»…ĵ¢¿®¨2!18¤Mг fùøø†?qDêDª¢_&;ŠÈÞYs@Ðb"ù•ÓKç½6'XWÒ$<eé7¯tݸ|‘ͺTOuÓm¤To­…ßýàcih4(8Žæ¦ÄpVAíº\ÆnŠÄÖz¤ëÚ2Ž&ÀÜЃ;ÎnÜM4 û@’ GKDñ[ø|·ÈH|°®—/…9@@—ê‰0oºÍÄn䙯/Æ%øc‹ÿ c~^¡gÔ®Ëeì6Ôê>ºvãcr®„ݨÓù#ñÙ¤è^Ѻ™}‹Yï3-fEÀ8¼âîºq)åºà#]ªCã¿Iª"/³ÌóHÎ*¨]—ËØm.¨Õ}t= ì†1ÆÄH’7a7*C:!^`12Zà³r¨Í¦À›àíºqm*ééëR’Øÿx©>²±Ûq ‹Å`ìV êø„t=àG !8ßMCÄÅ˱~ˆ×DÅv‘L½q­j³)p<„4vÛWËu©KéVw¼TÇÇØí8†Åb0v+u|B«z€5hÒMû³?û@T;‹ŠI·§Ïá>E9ÄëÛ“g1‡ÚP®Mãq ¤±Û&¸Z¼*ÕŠÜêâ¥ú8>ÆnÇ1,ƒ±[1¨ãŠÑïø±ØLJÃÇÀÃeLR‚Eê8ÂòÓèäœ ¸Pþ7Eò7v !ÓŒTë,z)ÕI3vKc™HŒÝÊà¼)•H=À¼k¢™ó’_ÅTÒ«…H= ëÝXàµa|2T„MC‘„üÝBÈtç)Õ^Vã‘R1c·$0–‰ÄØ­ ΛRYÕÙ»Ý÷븙†Ó—r£.âõ€[©ýè1GÕDjxÆn›àj9ðªTÇÓÙ$d¼TÇÇØí8†Åb0v+u|B«zÆqS]ÃÔ˜[Ä=Ú«„~ܤíoãõK­™d@’kõP›Mãq ¤±Û&¸Z¼*ÕcAÝ䎗êãø°@†póxÎ*¨]—ËØm.¨Õ}t= µùy7¦Û˜†ó{•(š!^p|öxãˆ%lïŽË f¨“ÑÔMðvݸ6•ôôu©V„võQ¼ToÙ…¿NŸw|ËÍÿÎ*¨]—ËØm.¨Õ}t= û4ò;iõ~¥€2¯XbPhÇZ V‘ÇåüÉÿ¼Ìù8NQÄ×uãª.HMe@—ê‰0oº—êM€ ¨þP€ÕýåÎ*¨]—ËØm“À— ¬ëöÅ‚#`йuÔéü‘øÄèÌT˜Â# ~eÞ “’ŸúJÙ™””±ÛÄÁ[‹ß{ï=:˜¬2`­‘íʱq62¹ˆcŸžºT‡$6Æ?Fªã1C&Ùk ­Npb?à‹ƒ[%’®Yà¬å2vSj¶Ö#]Hß Ž›4|ýh€\ÙM¦ÛöäbÒM 2¢ý."óìlÛEwn#G<ç‰Y- Ì\Æn1è5ÆØ­©ê̬êo$IßG.šž\pŸ¢ âõTåÙ-tއŽO\†éN³.p€°™¤Ǥ›[Îðä}7ú†ôã”?c7œ¾­Jµ"·ú£x©Þ˜Í»í†®â‹ÆnÁ%½ª0’tó\÷ŒvçxòŒQÄTzÀ"’Ih‹QÊPnñw_¶ÇåP.—Õ!Ÿ2D©Ä)ŒÝV!ê%ÀªT뢫<-Àn _и¨Ï*¨]—ËØM‘ØZ"õÀ´8.úkLcÑoR4€<Ú§ /ßÃZ<Ü  @GL¾y“éI`–-[ÿ4Þ®WL¯&RªWex`ŸTÇ 3(Áu¿oO𥳠j×å2v Êk½º¹6™n[ü7ïsP„6jX„j50ªcui@×k–ËzêRíEt‡ã T‡jDäÝ Ä0.1ó {Úmºn€ÆnAy­÷@ל à§ÛŠ~8¨V kŒY(0Zûj~e@Û˜ñ[w×kR–‹ßêR­íꣃRª—jxó–ш«_þ6嬂Úu¹ŒÝæ‚ZÝG×<½›h“y·áÃòî søÎT´ÁA="¬EÄÓXÈ:(Õ“êøåO~(k0ieÃv=7¡ 5LBŽoÏ*¨]—ËØm,¢¸WõSon¨dèµÑ bp ¸U%@€ƒz`‘°B -–¥sL·AjÌ»‘sݳëÆBæšþ«R#À‹aJõ¤:HáÄ’DÊv=øpMBŽoÏ*¨]—ËØm,¢¸#õý5¨AÚÇñ7«f“õÀ"a…@ vyFPÙkßB1àßuãRÊuÁG‘R½È_ºçA©žÔ Œ™O.wL†±Û©æoݬ¢U=0f19¤[8ŽïLÆRUpP„kÃP`æèlXú-q»-bÛ£çªT+r«?:(Õ‘`ÂwJȳ j×å2vS$¶Ö#]Pe2ZÂ/¹„ÝäWQñzÀm™õæÅÆYE>ñ\ºu“ò¡À´—ñ+/~öãñíÄÝuãš”åâ·ºT+B»ú(^ªóUÁYµër»åøÝ1ëz€§c.›»m¯Ø2‹c(ÂÄ zy-6û¨ßÿÑeó#/,‚= cª÷¯.üïºq-”çÂ^ºT+B»ú(^ª#áGæ¼@`gd2tVAíº\ÆnsA­î£ë1)a K敊6ˆÑ˜‡Á8&¿Ò=d†]?‡ÞXZ¬" ‰™)BÙ«n•À]7.¥\|¤Kµ"´«b¤zàDÈ7˜ÛiÇB(ËUnÊÀŠA~æ±UP».—±Û\P«ûèz€%41úV«­~ F°)vQÆü*GÕÌa!)™_Ã-{`½©–XÂ'½Én™ó˜½O×Ë—Â  Kõ\Vã}b¤zS0/,’Ég#ÿ|ƒñë>ù†-RG$Î*¨]—ËØm“Ø— ¬ëØ úà ÒøæïCÆëB’”—ñCR„Å&ÅëGzmN°KHùU»cè ‹ŠXíèuݸ&p]üV—j/¢;ñR_È'Ÿyw`OŸ#±˜M*[ŸUP».—±[¼À ©ëa·ñé¥cJ]9Äëún2ïF©C'à „¤Ä!¿úY!rúÛgŸý£€)gD*Àvݸ”r]ð‘.ÕºÜêOã¥zì"äú»gÔ®Ëeì¦ m•§ºvó“b25æe^ ¤ âõ®þKUF)#¡Ð KVs¥Î§²×k±D—õÔ¥:$±1þñRü³ j×åë™|Uo1oB@×Ân †.E!Äë¤Úç¦ó'àxOš֘7•HäQ×kµt—  Kµ"´«â¥:àgÔ®Ëeì–OàwǬëa7æÝæ“ø·™”l;«þÙz7LGØr±\˜”ˆäêIX¼N`±Hq«êÔ¿®—Z²Ë=Ô¥z•”Ænù„©ëhì–O0vǬëù¼›Òð'âõC?ÝùkwÑ®4P*s|órm: kø£ïÌ#ô>]7._ s€€.ÕAÝt/Õù*⬂Úu¹ŒÝò üî˜u=ÀS¨'·Í$I8KÈ'Ïî8îíKz[x.²aè62§Æü;‚MÖ€ ¸!0¦hb‚B`n•Yû®×¨Ôæ4vëRºn€Æn ÊœÎnlž¼:úôÿÊev ÁfM–§ÁAÎàÝÜ2ü#OÂ’Åq~é·Ü². T ]7®P¡®é¯KuHbcüã¥:ògÔ®Ëeì–OàwǬë–K³dŒ‹?Ä1ùUB¼`Íoί±Ø‡ÖÄ dÇIX4¨p˜›sð°ŠÚѳ3v Qs/êR­íê£x©ÎJ×, ÀÒu¹ŒÝ”š­õH×el&!é¯Ák!ƒIÔÁàA–!ààW¸è$f!8’`üS ÜuãRÊuÁGºT¯R˜ÀØ-Ÿ8uÝÝò Æî˜u=pÇnOž1fÈìdÁ/nBä6‰ Æ?C¼&EÛq…I7.ÆW‰‡¾| ëƵ[Nù¢.ՊЮ>2vË'0]7@c·|‚±;f]Œm&ÙN¢u£°÷ÀŠÝÁ˜n½…%Ø\ä]qƒ'aÍ–¸wFç ¼‹âÞÕuãº/„ýwèR½JaJc·|Öu4vË'»cÖõ€°›ØL¾÷Þ{Й쨜–݈Öïä/ ÙøÅsÑf2¦¤h'Æ!e§e02)«ÞðT^ïºq)åºà#]ªòZ}dì–Oœºn€Ænùcw̺÷Ý„ÝØ) –ÝdÌ“I1·.€.áÓçŒ"ÙííËa]¶ëë-{Š?)wÛ,Ë.+q[.wݸv À)_Ô¥z•”Ænù¦ëhì–O0vǬë2ìÆ‘Ì»Ipp‹Ê[ì»Ñy„ø>úÓßÇŠ’¾X¨;&kÜÉÐ¥,Žc^ÒT€êºq)åºà#]ªòZ}dì–Oœºn€Ænùcw̺pì6˜”ÐêÙw‹¾•ì¾…†ëd=y¦hƒx=@T~'8‹[Šƒý$]°y¹è<à›ïðW?ú'—‡°Í¤,pmçuÃøäŠ—ê|°ŸUP».—±[>ß³®x Ý(—¢ 6éhˆ¡H&Ý”¾Øî2Ê‹tåtÞìºqçd¯ëR­íê£MR Õ³ j×å2vË$íG¢Õõ€X•xvsÝŸ§ÏÇ—¢ 6鱨§“¨Oí.)Ѻ3Þ¾Tbèºq)åºà#]ª¡]}´Iª3!VAíº\Æn™¤ýH´«zÀmK2ìi Ç1)ÆX%€ßÅɸ±rˆ×wçÔ f*¤Âæ–GJ4~ºd{I9q€˜™z?¸»n\“²\üvUªÇ‚ºÉ/Õùªà¬‚Úu¹ŒÝò üî˜côÖ#~ë-¦Æ˜ƒ‹Q‘z€CâdXò·û?‘h9ß]"ÿ"‘8Fûë¿!~±?áCJ`âèºqMÊrñÛ©Ž‘áy˜H©ÎŠÿYµër»e•ù}‘ÇëÇ; KÓp"Ò!š·ý±O¤àHâ$ó˜úÓ%dm·Ê95ñÅ”˜Ù Ò‘Ú¿ß²4ÀÅük;'Â^CÆKõXbcÜ‘R¸®Y@A¦ër»)5[ë‘® XL &ÅA Ž‹>E!Dê;öùüSa7ö–tdôæíq@„(™n#BæÝÈ-=P%Ú®—R® >Ò¥ZÚÕG‘Ró³ j×å2vË*óû"×õO=—-:m¯ :ƒŒ2—“ÒCÜW–ù[w“†Ož}åK_eöPvA™Ÿ®W¨P×ô×¥ZÚÕG‹Rä”Äù¬‚Úu¹ŒÝJ6È´t=àl&INnm°¨sÅYÎóÉ3Ø >b¦ì>XpŽì>ÀÊ$Yk —[U§®ïºq­q±ÇHu¦ ©FÑ.ü/gg0 ÏØ3·û”º´ëxÊé]mèìÆÈ¤ KŽ-$Çî$ì&ÂDˆ7¿w·¯n¦]-µ™k£¯ Âq½øÙÇ·w×kR»-†›ÈÁn?ýçŸKñ¬ uÝÝKÝxÊ€$Wa±Ð£ø¾Û",²KäøÉl¡ìŽcÚ† k²73qÿëü!qØ“0ø9 0¾íºq bî’»¥B»ëhì–J Æ£³,Їœïb±rvªe ’âÂáfÓTvã©?9Îö™L(6•GÀØÍCqÐaìv@{}‚@ »‰ý¿7ž”±Jù Qþ9Ø »J&éøå¨²Aå¤Dã[gUòè1“zl5iûLŽ‘1w*ŒÝR!iì– I‹Gˆa7"t•d7ŽOå‰6:n.ë×ÜjÕÀ’þ¤&sy~a]¨ê»n\¡B™nŒÝR!Üu´‘ÉTb0žXv“͸ø}xf7¦Þ`46áÂÞ eމ,CùÈ#æýÍfrŽ’ùDÀØí €þuc7…9’ ÃnÌ»a'‰¡þü*Énd•.$7o(¢# «¹ÇßT, P^éºq)å²GY0vKo× p¬gRbñD †ÝªÙL>\ž7I¯Mé¯MÐ`bNg´qø®׸ æ.‰€±[*´»n€Æn©Ä a<:»Iw©´Í$ÖþÃß„˜8J€ëAÙß¾äÜí>nºn\Êm¯BÀØí|£—»n€Æn£šlÅ©³› @R§¬A[|o3éN^»½Û–ÛÆÑùæûð:óת‘ÿbÿÁ@p7²“ÿb1)Á,Û~gÙòèqŒ‘ÿbŒüe/e˜k±€Ø´Ð—dE˜ $ ppÜbàž]7®åµW’ `ì–F"麻¥ƒ„ñg·ÍÉjßãd1šØŠÈ¯œ^ºX@Îìfg-éµÑ}ÃíöãJ÷×uãJƒÅ´ c·mx…CwÝÝÂ[íÉAv£NwL.–YYsíF&=†þxƒÖqÓ㣸ÏÏ®׎òÚ+I0vK#‘tÝÝR‰AÂx²Û‘y·ÅR(Û|ûƒ?gg-ÿÃ’JGÏ‹wtݸâ‹i!Ó"`ì– Ï® ±[*1HÏAvK×w»aBÍgóöeh9þì-9ùcEÛÄg÷m×kw©íŃ»пÞu4vóõØŽ#†Ý8åml'9v§é»½y= £Ž\ 6:Ž«ñ×u㪘¥é0vK%]7@c·Tb0Ý /x'´¢ š“qc¾óîU«_ N)żDöÁÁùG%]7®’@YZcŒÝÆhqwÝÝŽT}¦wWÙþTÈnÄÙ¢#’Ýi’€%¥€n¯ãGe‡äLEEÛuã Êüs#`ì– á® ±[*1HO »mþ•QÊ Ç­²K¹93޲`Þï·Ø’ã¶‘ó„eŒŒªëÆYF –c·TvÝÝR‰AÂxbØM–Z3~È(åø—Û £oWÙÀtÓ˜eƒ=I¦ãÂ!+µ–12ª®Wd-XrŒÝRAÚu4vK% ã‰a78(télâ^e7w²¶œ÷ô9‹×Þ]½J–z1ª®×b‰Ì³Æn©@¥ƒ„ñİón¿ûÁÇóëý?ž0Úøv•Ý(Fþ“ ›ÿÐæZÿBÁß¼XðÜåÕuãÚUb{)Æn @¢èº»¥ƒ„ñİÛxÞmÌ_º;†Ý6„¸Ü< â>ÿô—?ù!¿ÌÖ%4°ìºqmBÒ'DÀØ-˜]7@c·Tb0ÝxʘdV›Éø²p™‘‰96-!WrX@| zÈ®—^4{šc·TØvÝÝR‰AÂxtv£Êè‚é#¡\|ß ÃŽl“CppxûÉI1Y5Àj8c݃ɥì69 ¶û¶ëƵ»ÔöâAŒÝè_ﺻùzlÇ¡³›ôݰ“dT0Äb!ÿxvcä“Ná‰JÌWóEˆð‡ÑØp’`¬1'LhÛ®Å×uÏ®—^4{šc·TØvÝÝR‰AÂxbØM‡Ó&Ž‹g77äøä=D…l|ú]7®}E¶·Ž#`ìvC‰¡ëhì–J Æ£³›‡bc¥?æ8hN®$ìYéâ±0ÌÈ%YŠ|}5X×kµt Æn©€íº»¥ƒ„ñİ›ë© »ˆH×iü[’ÝèÖMNÀaQ–“©Ðèºq¥Áâ‰AàÕ›·h3xÀÂn4܈³WjL$f‚@× ÐØmR›-Ü®²C‘þxxJ&Ède·nK?2‰v’÷³lwpômd2= –FчȈóÕ«YŠŽÃmì¶jc·}¸Ù[!tvã©ï©9Cus’I?.9»12‰ê€àîØöésHÖØ-T³æŸ¾Õ‘ÆÛÛ›1»½÷Þ{bÊ›5é³FnìvÖš­U®(v{ú|¯ Í%g7¡Z±–„×$~,'SA×uãJ‚ʼn4é²yvÃø‘1X° ]7@™œÔf ·:»aOL:e‘·ÉÙ †¥§Æâ;Ö0ø;¿÷  í3Ù‚]2H8» @s·—#A¡Ý€hQŒÐÙ6ûÑŸþ>TÂ5qÈ­ÂtÉÙÉŽðÖ›hF)™z•æ³ëÆu¨äöò.h¡ïÄ1&ÉÈ䮘ì%‡@× Ðún ±În2è§Þ排ìæÑÃrò¿üÏ܉9ÃÑ<Þÿ £ëÆu°ìöúd(R&àä—æ°#{E躻5(Æ:»avÇhr›?èMnŸ<«Ân¾-0鯒·T¨vݸR`ñlBàOþø!§GÇ "´éu Þã†ÑHÙ¨„_&Ý&+à|¸rb"º·+¿‚üCmr™ð”„® ÕþÁÚÏñz »Ñx…ÅpÀntèRóbæÝØÎ‹ù»q¹~õË_`72öñn„ÿû_äö'‘ƒ¼Ÿ]7®}Eîú­FØ Ó&®ÿkÅÊQYÆn9P=g ìÆ×¯g7†%1û_,ìŽ)Ç匃Eº»n\‘eüx±¤,˜®xóÖV,buOc·2µ\ŒtŠ%”7c·¨Œ³vŠpsg3‰eàU²×øyhÛ®q˜Hw×+²Œg fìV¦6‹µ‹b åÀÍØ-ªãlݾøü~Ýv#Ü. Ë4žÊ†]òûý¯? ml²ƒîÙuãÒ‹vʧÆneªµX»(–PÜŒÝr z0ÎØM#Ý\OŸs°˜n±PŒXÊÚF/e?.eÛ®ÅtÏ®—^´S>5v+S­ÅÚE±„ràfì–ÕƒqVg·W¯nà,æÝXÓÍ_8¸Å\d±\̲A‚П HbZÉíbÈž]7®åíýc·25X¬]K(nÆn9P=guv»G›UolbÉ2mØmqE€lØý¹…Ož1,ÉfBcì®×A1èñõþØ­xÆámyÆ¡bÝ›"߸X»(–P,¤[»mA«PØêì†HCUÌ£qF*}7( v[,< Ã¤×ÆåOåÅÅÀ;<»n\;ÊÛû+°ÒÈ2.OPP•}x^ÿè¿Ê+Ü*ï9#«÷ M(éáË›"߸X»(–ÐCäÒÜ»¥Á1i,ÕÙÒ`çÁAjìN Ç1û¶XDwΣÇîl'ÈïbGo1Ý³ëÆ¥í”O[`7ùs9ð?|ôj'½"º÷á•Àܲ¾Õ»Q@Æ™S㦠6r„!ÀøJ…O×+ÅÓ»¹)c>ÆF:G†Ü_üìÇ Œ¤Ë(m¹ìÆÃˆ½¾-ϦÈ7–Ü–QÝ]7À2©c§4Ân Àˆ ×mx½_°\ãÐNmdr È¥ÜÕÙM ¢~þ«Øå6då, =Æ ‰ýÞ·¾Æoh·p‰h ›Ñ )nxy:ùÝù¦À$TŒtŠ%4A/É­±[ÓFÒ»±L›^ÞqËd¶}FsìÄE0Ô‚¿¾û_ “®WúФ:»!0ˆŸÿâBŒ¹ Ò¿CzYÃB/Œ!±ŒR÷‘ßýýö¥‹üÕòi†›"ߘkÅR`ßýÈØm7tù^lÝ`4–°1VÃpœœ'‚*˜iêæÝž>g<‡_´Þ2vË'Ç\ÝÀñCP…à„¼B ‰m°i>Þ¥ …wã‡Ožq&H*ˆ}(ð¦È7&Åb¤S,¡ŒGüÝŽ —éÝØÆûÓþ©¶ÌP n<1Œ—š¾Ìk@¦ï¥ŽÃqwݸ޼Ów[`7ÙU€y10ÄÍ癦Û|` ,9Ç6TóH΄¢«EGI½„‘oÊI±vQ,!ö}OÝöá–õ­ØÓ‡C·u¹¡‡ÖâûvÃŒd\v¨ îC“¸1ÌýÜÄ8Ønw×kw©û}±v»Gωî½;øž’ x ë)(&TlY·)òM‹µ‹b ­À¾ë±±Û.Øò¾Ô»ñ [Ákâà×9ž>§ Žÿ`7¬£±<‘oZÀâ¸ñ‹‘î®WdϬ v{û’Y`ÄéűÆq7Ș˜Bx2ð>¯Z[C7 c›¯êº)ò ‹µ‹b Í¡>îcìvÃä1´ÀnôÅdªÙvñ•;ù£+'A2ŒÉXÜN‚í¾íºqí.u¿/¶ÀnX?"½"Š8¸Uð„Q—Wè¸A^:!c˜r8aä›rR¬]KH©£ÝŒÝvC—ïÅØm˜PX?'s×+78 Æ_ÝœáÇhüœ¯/nCFû>0a°'áã Ç °~“ÂWáC6“›"ߘìkÅR0ßýÈØm7tù^lÝ"OÀÉ‚ÄÜuãÊ NƒñWg7Yà6ê¹õ×! ¨_ýú„Ì»åÇÞ' 3ðî¹òÕçŸ:v „ßù¦Àd©X»(–Ðç$·ÆnI`LI ìÆ8‰3âZ;'mÁç±uݸæÅ9½OuvC``¿Þ ‡B@²†ZVhB[ÌÓ±M¯#b#$<È3'h #™ËolŠ|S`Ò+Ö.Š%´ â1_c·cøey»:»É0ƒ0rŽ˜A†F`²@pi×ë¾ú_ÝÀš78ˆã*ä ]n• `tQ¾âøãRØJ"¹ ,›RòŠj@µ)òM‹µ‹b )u´û‘±Ûnèò½XÝî­úoœ€Ø“!ÄÜuãÊŠL›‘·Àn ƒA4Ä…c(dŒÑE1ȇàôðÒ‡' =ü¦ÈãkÅÒaÜ÷ÔØmnYߪÎn”Û0 ¤Y£}wΓgY‹мëÆ*Ô‰ý[`7FÇóní¹œ}ƒ´+G2Å6,. YF7üK´¨ooc"÷1Ä.Ö.Š%äAHè0vKfª¨Z`·ÈpR9O×+T¨û·ÀnŒ2•q­îz*s|˹‹9µÀq9œÅS&æ˜æÂ!—>2¹ÈæÀÅÚE±„r4 c·¨Œ³v£Ì»1À ŒrÎÁ’®¾ÞuãZ-Ýù4ÂnP•¬»tÓdÚÔØ m„”‘Ffè¸Å=ÿ“¶@spÓs÷KAq°¥Ï<ð½OläCøMËØwÝÝîE±¡ÿM°ÛƒC‡oÜ985þºn\5«œfuvC` 3¾Ç<P’wOb=Å,³øËmhù€„þüftw‹é¾øbí8¶ÈÈ·æ¤X»(–Ð"†=ݘãõØÍ}Ä~þé°´çFÖõä(éjœ]7®ÕÒ/@uv“…cŒòÝM½yq?_¶¶PáØ=•õn~9Û ÃFâ~·®_þä‡0ièxM‘o LÆŠµ‹b -¢}ÐÓØí €9^oÝh¶ñó 9@8»n\ù`i6æêì2r“Ì‹ñ‘¦ŽLn8.G0'rbÆüƒ‹¢ÛX’4öÄ¡Xn \¬]K(‡H»å@õ`œ-°›œ`%[Mò««ˆƒåU^ïºq)å:ë£ØMfŠär¢«šûÊô\äq9Ôp2õF‹À1lX¬ÌM‘o \¬]K(âÆnÀËõjuvC¤ù@õË`e´'WiÕx»n\jÉÎù°v› «Ð$ðh)ÁìɽìIœ\‹ö'÷¡ÿë L^ÑkÅš>É­±[ÓFRÝ„Î>ûì¥\ÌŒëƒ*i‹?Ž­ëÆ5.ÈEÜ-° ÜB†ý¾˜2c'$·œh#ƒ>Ì¢#yäûrR¬]Kh탞ÆnÌñzuv㣔ñœ±llÿLML×+ ]ÅR—Ý` Œ{Ù‚€äpÈuûöåB1wôçq3)†™WFà3E¾#'§X»(–Ð¼ŽŽûŒ5ØñØ,†$Tg7JÁüB-FcØuãä"îºìöÍ÷?p %×°o$nl?æà#Û2ºÈ‚5 EÄ-;Ì‹O¦Èwä„ükÅ Á~Äߨíz™ÞmÝ2mk´]7®­…=AøºìSp±9*-HÜÐݱ Ñþp'àxäå^;v Š#ß’rU¬]Kh u*·±[*$ÆcìæÁìºqùR\ÇQ—Ýço}ëÿþ“?þ#q3œ»!E‹U çÎ|ô§¿Ï&vûÌÖ1D¹Ò{fŠ|kNе‹b y„:ŒÝ‚™**c7d×Ë—â:ŽØíý? `ÿÞ·¾¦/I“%n0sØbK™w“JÌù¦œkÅÊÑFŒÝr z0Nc7`×Ë—â:ŽØmŒ6”á7ÎûÝ2†É/sÍ«¶Á¤7yäñ9)Ö.Š%äQMè0vKfª¨ŒÝ<’]7._Šë8a79Jë}ÛÀ°½ê«¬#à œÉbtâ¹p»ùƒWÔÀÅÚE±„”=ѱ[" SFcìæÑìºqùR\ÇÑ»1ƒÆ£» }í3nȘTËŒ²êÙ77™µdUÂVÉîúbî4RÖ+‹îT'Ïî8îö†ŽžcvÛù¦À>oÅÚE±„|Ñ:ŒÝ‚™**c7d×Ë—â:Žêìæwc]Œ#¶ˆxNªköš“ÀðTä–ËD"gë0C'{5ó®ô½ a6E¾)°/E±vQ,!_´„c·„`¦ŠÊØÍ#Ùuãò¥¸Ž£:»±k=)Îlv£gänß¼T}:º`øËfËï¶\^뻹ót^Âh'„¯LåùsÜHhSä›ûRkÅòEKè0vKfª¨ŒÝ<’]7._Šë8ª³PË!5ôª°–d-›¾"`\5¾ß7öœ¸áJé¯ ¯Mž*·1‘û×WkÅòeOè0vKfª¨ŒÝ<’]7._Šë8Z`71ì§c»Átã1C½"\¿ïés= ìÆÙ$±õ/&rçjàbí¢XB¾ì Æn ÁL•±›G²ëÆåKqG ìæìC†]³dùØÚ¡6š¹¹ÎÐMênyõœÛ¨ùA<÷o=ð\‹|Càbí¢XB÷ˆ¥üoì–ÍDq»y »n\¾×q´Ànl›üóOÿÂjóöåê¡6›S Kº5tD>\Ìë‘ spÿëïÿn^Ë›"߸X»(–нã>ÆnÇ1Lƒ±›‡´ëÆåKqG ì&ScÐ;ÔfXø¦à¿)0ñ0t)1Ëé9ü²ØHÆ+|r›"߸X»(–-¡ÃØ-!˜©¢2v#i":F£qw ì¨ `n LxNŽÃØRNÌÁÁ­D²Èn›"߸éK(‡`›êÈêÁ8a769Ïq±Ã95Ž JõKT4ð„.fì žöz1ª³ÛÝ€Wïæ°^üìÇ¡âo ,‘ÐÃòSo^Ï-þØ®xOŸÖ¦È7&‰b¤S,![B‡±[B0/ œM!”S±.ˆÉ•‹\ÝüQ2lüeðË@b¨F6–H 36'7«Þ„Ý0_™ÿmŠ|S`Ò*F:ÅšxÜÇØí8†WŽÁØíʵ?/{uv#K¬Dsû‡<}ÎX—Ð<«â³)0¯°€YÓÍ…ƒÛPÌøoŠ|Sàb¤S,!ÆÝŒÝvCg/‚€±›‰ÁØüø£dX8ÆìØ8‡s7ÑáüFv4ÄapOŸCm‹½¶q[#ÌI1Ò)–дTnc·TH^3c·kÖ{¨Ô-°g¸÷«4>.e~ÿ^ðÿ8|0ÐýƒqàÕÈã#b Ý–ò¿±[J4¯—±Ûõê\+qevcUõëO0sÂzêöÍ‹»UiZ~o7—CL›Âç \ŒtŠ%¤ÖÒ·Æn;³×ŒÝLÆÔe7HͯqÃ!KÒ¼È8ŸÞÍ #úq9>0ŽMáó.F:ŃœÊmì– ÉkÆcìvÍz•º.»±ý#+Ñ8à›ï€ƒ[7G¶t¾›äßoVÌ+²%ñ •nSø|É^1Ò)–Pó#þÆnGгwÝLÆÔe7É =8ÖŠŠ[(†¥ãLz÷«W7Й?.GÌ[›Âç LNŠ‘N±„|¥$t»%ó‚Q»]°Ò•"·ÀntÜȆdÒœýä™’a\EÆ—³)|¾ÀÅH§XBJí~dì¶:{ŒÝL Æ´Àn Hújà—÷?Ô–¤’%0 'xõ¸œMáó.F:ÅKQ*·±[*$¯±Û5ë=TêØmœ·ñrw¬öÒ \‚Å&‚Má3.F:ÅZª™£~ÆnG¼öûÆn×®ÿié[c7Ÿ?™ó·ºcS`¢Ú>Iàb¤S,!½Fö=5vÛ‡›½%»™$Œ0v£±è6v[„%‡§±[T¯§±Ûuê:¦¤Æn«(»­B”*€±[*$¯±Û5ë=Tjc·2ÞߨÍC‘Ûaì–ásÇoìvîúÝZ:c·UÄŒÝV!JÀØ-’×ŒÇØíšõ*µ±[ïoìæ¡Èí0vËð¹ã7v;wýn-]³ìæ÷ÅŠ)ѦÀD¸)|’ÀÅL‹%S/[ûmEÌÂ0v£aîÙíõ'Ô Zúýÿ¹¼׃Z[ <ìÉ|sÿÊlÑœ*òûh‡ÿœwÎv1Ò)–Ѓ²'º1vKäE£1v»hÅŠÝ»½þÑE'Ké(±Q ³Î{S`³/åwÿàËr67nFÎ.—óS V³]ŒtŠ%¤À¸û‘±ÛnèìE0v31#P‘ÝØ*Ù]_|ã|ñùnoéaÝàX$ MÇ$6¶íúòþˆËmáµÄn›"gK—aþÜVÏ7w¿lûœ#b ù¢%t»%ó‚Q»]°Ò•"Wd·ï}ëkÏÿÃ#™Õ¢¿†› Ç"m ìË‹ª'6v¤vîZíܹ°›äÖÿ†²=ÎIÕmìæ17ÇÕ0v»Zëå­ÈnåFgêÕ›·0Ž¿ä|·yž7ö¯ÿüWÿ »qâ¶t oß¾\Ü»rSäÄésë¡lûœ#b ù¢%t”ùH˜a‹ª)ŒÝšªŽê™©Èn‹eOb 8Ž™s 8½c8ïÝisRŒtŠ%äJè0vKæ£2v»`¥+EnÝ\oëés%ÃãG1ddR:Y®‡¥·5ržœÐõ·sG1Ò)–мŒÇ}ŒÝŽcxåŒÝ®\ûó²·Änb¡q;>×fžá{ŸMï_º½ýäæßßÝ]›"wÆ0Ĥg»éK(ÞÆnÀ³WÍfÒdà-° ™S¶<üÃÿóFûÌŽ=ÈâÛM‡WoxËFYðo?ùða|î¶E>Ìâ‘gr>LçùUuâ”b.£º‹%´PÈÃ^e :œM‹ Q¬ïÖhÅTÊV ìµ15ö»|,#‡ð…ƦÀÄClÿðÑw°ŠdL’ÁÃa1]†6E.q’mÖ9·J¶‹‘N±„”Âî~dì¶:{ŒÝL Æ´ÀnPÛwÿúo$Wõ£â–­Ç™»7¦ËFx‰óÅÏ~üÙ¿¹Åݘ‹Œ#»ã#÷1Ëë’İömß;w1Ò)–л²¥s»¥ÃòŠ1»]±ÖÃe®Ên7·o^58å—?ù¡ä·,˜eySà»·ÅîñW¿þqâõóOÿÂE¾0ó¶9òû­•= ߳›ÅH§XB³ Jàaì–ÄëEA—¦'ìÆ¯`ðÓþ)ÍázxX‰ï¨Ènp'|ýÉoÿör£‘¯?a£æÝ~ã«¿9¯žMýë°$¤#p±}d¿ó{ßðO½cGä´bVÒ¹hpxó.‘b¤S,!^B‡±[B0¯Õ·¿ñ[è1ÊK{G™ˆ‰ûo?þËëà`% P‘ÝܦOž}åK_åWwî¥Í²6—‘µn®§ÏåæÝÆÏ{_ä̸Áhßÿú#.ÜNãÝ#b —Ìiì– Ê+E„Ø@jH>…†éDŠà5<•á”+!tѲVd7F±ÏŸ\¬Mƒé敱)ðƒ×‡Cxo97ï¶´˜nwä²É ì‰ãA¢³›b¤S,¡Yx»%ñzQ0 ‘I7Á‘¢o}ëÿæºVâwTd·w™x肃âÿVÓYcL’“X €c±ïJn5r‰“åÃ.Сhœ1Ò)–VڽόÝö"wõ÷<—IßÝ<ß]š —¿AvK[Œº‘OF&‡ñOnSÅ/sm>ZÖ(1#b )…ÝýÈØm7tñnò׿¾ÑU^™+¿v£$Ý43oª±X/›‹Ý>k{$rúb¬M[ŒV<·D~C7ØÜ˜ç¿ß2éÆ-ŽÐ_1Ò)–P¨¤GüÝŽ wåw{è ù‘¾÷'üGWÄÊ-°bI—б9\·²plS`±™”½DXm u΋A|ä²"€y]n•)ìb¤S,¡1n©ÜÆn©¼`<ÐÔ¯¡IhÈ~]À¡°" -°›¾ÿáÇX˜È(¢R;›]*±úÀxruåøÈ!XØw3e%BÊÅH§XBJí~dì¶:{‘ocHM.hÎ1Z`7ôüæÆ8t[Žm_¹EÖP[6ŽñäëO¥T*}SäÄ u Á ‡*1#b )…ÝýÈØm7tö"’OÇMØÍÉäZ`·¬á» «êXLq¥JŽI7.ÙC ‡'èÅø‹‘N±„‹yÐÓ”ÒA/þ:ò#ì¦Ì\¢Kÿôì&Ý1¨‡‹Åt]•ú-F:ÅÊ£±[T¯§,|³enשq½¤§g7_Ø„íDØì ‡œÅ¦cýÔm2É\Ñr¹ñÏð_1Ò)–P¸¬ûŸ»íÇÎÞ€Úl÷-“Aàôìï8KV½ KÞôsj6IŦ³uŠ‘N±„6aØØ-( Bj£ „žšÿ¥87»Éz7$™nFìÝ6_êrƒøª—˜ãÏÖ)F:ÅŠÇ*>¤±[.pyf_/òÑc²Ç—~ú@±R,!]B‡±[B0KFUW¼+)Ÿ\©ÿºnS©Áè)¾ºb §°ñ#'Ôø‹[(cÁM} ôȈ>zw¼Î’Í$Çg¸ŽØ°v@„²«O('ìóÌD« \2£Gà¦}ÊÅH±„/ѱ[" KGSWøÒ2œ’ãX·®Û”炎ºbÉx FŒX}øËR`·M}UB@ìUÂñ:œ÷¤¯C£ $=;Ùr™µÌ£aoéã™;`CWü_}þ)Ùæ8¹y0ñ)Ö@Š%*éc·#èU|·®ñ§IÒŠým*G×m*=ÆSW,!,é1yèüúkïã›û· )úVbòáþ©wHä”ôÅty‹väÙaIhÑÇ6wk Åš—ñ¸±Ûq «ÄPW0ë-{½2ò#ÛÉ:{°µ©ðx ºnSñÅ<_Ⱥb‰}þÄDZ¡·ˆó¦À‹1(žtî˜tc-~'Æ'ýQt®ú¡ÖÑ+Ö@Š%¤€¹û‘±Ûnèê¾XW0,ÃÙœBåæÍŸ>—£©&Šå>]·©#ïýݺbyÞĪr{êîÿäéäv8þöææöÓ.[ôç/ҔĊ5b )…ÝýÈØm7tu_lCÜ2dónu%¡©Ô[KÜÂ1Lëß¾ÄÄ1´"@pÛ8jpàø€¡ Kw1J©uúÎnXJ $WŒtŠ%¤v÷#c·ÝÐÕ}±5z»Õ•„¦RoA,±Ç€G¸¼™½ѦÀJ<“GÞ öäÎ}‹¼Ë™qÃ*w§XJàb¤S,!)WÚ_c·´x‹­5BadÞzLÇaI‚-Ú·0úA×YãPÔ'Ïd5·ô:cÏb ¤XBJaw?2vÛ ]Ý««‘¬ÅïºMeE¦ñÈ«‹%kÄ öãb‰äů2Ûµ)ð&äÉéÂVtß°¿ÂíöYÚ÷€>&aˆ\:›ïºœez’b ¤XB›à lì TkÁª«‘¬€tݦ²"Óxä-ˆ¥¤ôô9£Ž\úߦÀñà3M *^ÁÒ›óä™nß2ŽÜæÝÆhìv»í†®î‹-¨‘|»åÃ6k̈%„"×Ïõ¯tô"ÇÞ²ég›Â›ãtÝÜÓçcÅM§Rߨ¤X)–‚ÆîGÆn»¡«ûb#j$]·©L˜tm b¹é(™øÀ³³lYê²E3³u8ôÅH±„ôòî{jì¶·êoµ FòÐu›ÊKû1WKºWþ(–NËi2!Ô6¾½¥h r2ƒ†éþÀ>ï WB)lò—#rèß¹ëÑcn•׋5b )…ÝýÈØm7tu_¬¬F†Â#ùc&·ãG[Ý]·©­…=SøºbÉÞ~›Þ™s,A¼)0°$ ÄJ„I´»˜!ÐDDÈh¤ “âàVéëk ÅJ„âƒhŒÝÀÑÏM]5"8ñ‹é5òqncð#ð°{ƒ¶ë6¥ììÏêŠåä(ne•â4~S`^‡kdHbÃ\ÄŠ ]¹yÌ;|h>ÄæcNeÊb ¤XB;@[}ÅØm¢6ÔU#‚ 푵0!ù0c€Ifj É—®\*̱í$~¶—ä¡›zk ÅJã8c·1¹««KA{H{ä—3ˆCJ`YÅÃé¯~ý ˜±…—P†Íñb‰´p¼µü∹84-&WŽçÕ£dâ»%lómÆçœ2#“ÃÅ0äF£?È…Ãa1p1Ò)–Ðb1z»°Öëñj$FoLÂDª·TvX6물p„G&ïˆ>ÿÁæÆ}Q«kºnSµD¢…tcÄk@öϧï/vöˆ¡¹H±¼ÇaØ™Í?>0›k-n¥uÃÿn\bº3Éú¶ÿ¦Œ„ ÿúÜt±R,¡ð%º1vKdéhbÔÈ„³âocÔˆtÇ8èMZ"§YM–¯NÎÜΣnjíðûíoüÖ$Àø¶ë65.ÈÕÜ«bɇˆ20¾‹[%¸±¿aÉb{ºCuÝ4ÊߦÀÄãbƒ¼ˆ™Ëí{¼ôGGÅq~ý!¹¥¼KaïüȰ[h0¬bÀAæ•ÀÅH±„”Âî~d춺º/®ª‘x.›‡ŒQ#ˆ=ý54zI.·Ö5üÇG)Ÿ¾rÎ)ãEzKïºM…18ÿ],á/Om ¾!N†å]ÈfYû&þÎröW¬KhVÄÆn @¬ŪÑ…þ4Fa1ƒ†^¢2\³ø‘Ì’A°¬‰ÆÂŸ f*°Æ$SÁ§ÙG«bIg_î»=y¶JmHlŒXÞ!óæ’É5[t¶„ܦÀ£m“‘R—ÄÒÚRÞ0XJ÷m)ì²LÇŸ¬»™‡(Ö@Š%4/ãqÓ!Ç1¬êÑùK©FÐH2&6ÌŒÀÌ¡ !èKgpÈÚ\óÀÞ§ë6åKqAGŒXºeÑ~,Sl z·.“<Kæ‚Ý0à0˼1tà*bº¶ú®r6æbf˜bˆóFÆ+î"zøï7Q­2'¯ËüÃîîŒÝa‰ô4v‹ªµ`1jdU]„Dªœa|†+dÃÌÐ¥œñ7JcìÖšH¥ÈOŒXÂhôô¹dN ú(Žý#Å*ịÁ=7røä™¬wƒ•ˆj^ÄMy¹•y4^ΚljߔRHÛ’PY|Ý{»y(v8ŒÝv€ÖÂ+1jd¬6¹#ÕÈ€Ã쮀UÊd ™Æ0Ý<ÂÛ—0Øún 8-?ZKˆÌu¬†ÎûøÊXÑH±$ZoNϪjÖ‘ž°êºMy]–ióy†+i²²“½T1­„p·þ»mElÞØmŒFGîU5²ª+”‘j^ãóeÅ…ƒA@žÊ·.ú„ðŒimÞMA§áGºXÂ/žÚ0º ‡å·SïU‚™ ê@zû‘#KXã#uŒˆÎ‘Û˜."„³$¿\n­ôÔ(,còtNYèG}Ì<|ŒÝa‰ô4v‹ªµ`ºQ˜+æQ$»9¶zúœ†Ì…²âVA‰sÂ’¯Y4|tE(¼õÝBÈ4¥¬æ†hèõx9„tÇ ½9ï¹èˆK!P¤Qü:ÇÓçb§1pS`¡3 ¥$FÝ‘äI„þ–äè߀ÁIRã(£é0v‹j1˜±Û",í{êjdQ9Ä{ƨÙ]¶¬ätc:wôð$0º"¸ÜbÕP̺X2HˆÀqQL¸W Ù†MdäÐÏóâ n a~¬3sŸsn smëÈÉ[rkì¶K¤ç¸¦"_±`-  «‘‰öØzÃn²"€1ACìÇæì†¿ ÈкïŽ6¾½eX-g}·)mt±dú‰zgNj.Ò‡BÍyŸ±¤8÷ó¿|h…¾µÞzS`^ƒ%F‹Š|Hæf0×¼¡£§ï3ù.O#­l±{Xlp£XB£B's»%ƒ²lDºñ aŸ#R ©o”é67ð²´ï«|–cIÂw; ¦ãÂ!sý!̺nS¡B]Á_Ka7jŸ ¸±d2M†,qÉ2ñ£±;R,!5f»Xe‰pâXã¸MÝ\ž,oaäs%òן ËENØ’E€‡{ïƒ[ìk ÅÒÁÙ÷ÔØmnÕßÒÕÈX'ìpǨÄÞÙ<}Îz%ЀÚÿ™ÃÂ$…“áºß¼èΡÎAtݦæ \ÇGK·”{`1¨C}ùÔ‘ )ü1ðÐe5F,š˜¼‘"w¿" T›ûÈåP-ò7/ 5×:†íJÜìÛÚÞªPáÝÒ¼Û[çåÿb ¤XBJaw?2vÛ ]Ýu5¢k‰Õ§‘j„¯J¹€"4¯Á#Ì;¬Òtݦê FÝÔWÅÒͽû}òLï¸!´1b駃é'ÒCdª—$³¶| X)–Pަaì–Õq®ª¯v8"ÕÃ2 ÑÈŒì5*8m„f++ã$LhHGžvݦB \Á?R,é¯yŽCùcž#¥‘b G¡¹p« xë\3û±ðíGÓàW®¨9þþÿöÞæU“ä¾ó­Ú ¢þ/#j!tÁ;Á,îlŒV µ-ÆÚ4ƒ bV…Ô4”Ý ]$a1ÕBpÝÈæÂŒÐ -ì]Ö‹ÛÐàé¦í¦ßN©ºDµÏýäó;'Nž|ùeDfddd>ßCòœx2#ñ͈ø<ñŽ,´I’5}Æ'2b¤Øƒœw4û’è6[ºmoŒ,Fb ¾ŸÈb¤£€ßÁA§»äØtžÓÚD»o}Ýužº“3ûŸ,ÅÁ&hqÔƒ&OF&ˤÞ$ϼL Ó×Lì8ðæ8p¤Û¿în§‹fh%–é(´óÖìÏçmo7ߊebº‰[>—è–OË¢–â‹‘>¼&ÏD# ª®ÿèY`±…ëoÿm~ Úaëw ø;Úuž‹Ô9œ÷“%u4(¦<ÛºÊì$ ?eF&KªlÔ}LíÉÞ$ÏëN± úìˆËàk¥\µ‰×tk<ó¬¾gæ,XÆô§[.Hü4Ø"Hßs8S,ƒ{PˆZF‡è–QÌ’¦übÄ/%&¯F#GaÀód1BcQ3kàþCJƒ9=¸ë<bq†?YrÕ@6öé§Ì˜d™ÔÛäÙÞfd_3A%wÈí·œ9E”9O/~pðiŽAÏ!EË Å¢–Ñ!ºe³¤)¿ñK‰É«~1Â/ðfM¼ÓÚAüÈ´áa6³uL+Fhœdþ Pö˜“cþw§Æ"uçýdIiÃëŠüôS¦Ÿ,MÞ&YF÷ð&y6û‘}Í4BSë’ö;¦É mo7žUw[œaD·ÅncÀ/FüRbòª_Œ4ó¸ï= WZ]¬åe +çGÃC£Ûë#‰nÛ$5Ÿê'Kk™¤ým2zð“eˆ?¢ü1KÁ'Ž$ÏíÍ=Ö×LúaàYñ`Îñ\ìç_±9‘}It›-ݶ7úÅÈ`áÒ/FBÎ5‡}N®®i~¡Ÿ…º[3øyh»œ é®óTˆÅ:üdi³¹'÷K¨~²,¥vÓà@/³Í“\:O§[¹×³|³©MÇóŒ¯Å2H±ÍaòÑmR¢:=øÅÈXùy~F1ÒÐÍV³tÞi_ÂãB¿ÿ ¼»ÎSƒ1:““~²ä*ÝI¼ýÈtØñ6#Yf—Ýæ/ؾ9´]ЭÖ^L2<Ž ²WøŠc²cúäùª­¾™ýÝZ£²mÇÜÅ2H±õã¸üŒè¶\ÃM,øÅH§XHý:¯k¥ úÐ2C{ u=Hǧó·ë<åÄëð—üditƒÐ!Œœ Nú u^²Ì¨9ÐÎ õçóÝû'Ò3Ž0°ªý †JÒÅÜ,„rÚцOjyxnûé¸éÂFš50ÈÀ’±Ñ˜vW± RìA5²|ݲÈXÞˆ_Œø¥ÄäÕÈb„üØú…9µ½|Šÿ†k–åÇUÛužÖñ¯øÉÒèF!?vø)32Y®§²Õ¿>ýücÂO5`áè?ÑÚ`Ãv61›ÚÐ`‹Yj|4Ý›>aʾýb¤Øƒúq\~Ft[®á&übÄ/%&¯F#äA~aZ¾:R0–€Ÿ¦4ìðëG ™× °.ÍPÀO–7tcÒÐá§ÌÈd9#Ø‘·ðÃÌÒ<Ÿü´£†Å(©Á{©»1(”Ê v4-ón¡Yƒ¦N*†6^«’¦{Ñmðýêäª øÅˆ_JL^,FÈ­t¥Åí€sg[Œˆ]½&W%ÚužZõ½WnÜO–F·0ª„’<$Ŷ;œì8"“媃šù‘6ø8–aáh_šì˜Æ³ß\ß¶V,ƒ{P;v¹Üª»åR²°¿é ©_cŠ’=À «‘11Ì×3.Q@£ Zqï©ë|X¶]ç©á(ÇY?YÝv=ª„×HR·ƒJ˜|±ÔÚhÀl_"IOvL·ýûîb¤ØƒüøÎ»*ºÍÓmó»üb$gÿ1t3`ݬJôò)M”c²GÀžiÉiŒ¿|ÊWÍw“k¿çýdIiCeŸ&»Nz‹ü“,W—®½úÜ_\osÓ},1¥)ždßü}þ™NéÞ?õ«eŠîbšŠñœëe$š2Ýã*à#‘ÅŘ·Èb„b HÙÚA4Ñà r3ØìÞúÖmÑÚÆóÈ^³°ë<5&Â9œŸL–LèfÔÄØçX‚´ó‘ÉrU›þâÞ;5;\àKóŸô23r’Oª«¶¾V®°Ë Å”K™¶Ñ­­ÆŽÜ“ň_PøW#‹Úgh™¤A’Ãßa­o[ðI¹ßJ³ë<µ£T”=¨~²ä*e¾sdI–Ù#Õ6Π•ý¢s€ÕŒ!9Íîå6¶drTIû)“îb¤Øƒ&£<Ãè6C´nñ‹¿”˜¼I·“WÃû_>õ•$Ú®óTRLæÙO–\…Îá§Ì”d¹–®ñ;æ5~ÅÁ¸f €Óvጿʬb¤Øƒr)Ó¶#ºµÕØ‘Û/FhùaÄ2-ÿÏð5K1’´© “ƒèw æÀMÆ|×yj,RçpÞO–Wt»ÿE†­Y›ñ“  ¡À‰zY’åz:“,©¯Z{„³üå*¹oüä³%MHù“ÛƇ¼X)ö ø¸ÇûÝ⵪ʧ_ŒØdÒµ$ÓDc )“ßqñ‰hj<3Eîg2Θÿ]穱HÃy?YÝlÌ$1“—!Z˜¿\9Ý gï¿ÿÏö*mbÚàke¾ÙŸpvXN$ 8i~ÐÎØÉb¤ØƒÆbºä¼è¶D½ ïõ‹‘Ýø…l ót–#Mmh‡! “ý5 ¿r ŸÞ7<Ÿºæ½7+eKS÷Y?YöéÆÚÑ­I·÷´S&gÿšÌë© 6!”_€8D·A¹V:Ù~S+=Bf×PÀ/Fx­Ö©m?ùÉØ-šJø‘Ìᣫ1,rë­Mm^¼Œ,Ë’zð ÝðÃW­U2¨Õ®OúɲO7«»Q·„ê§Ì˜d¹¶züŠ#ZûÑLvëÌwk®žzßÚÞ‚{ˆz£»rW±ŸÅ¤ÈèÝ2ŠYÒ”_ŒPJPb\õn\o¾FbÃÕhQ\^Œì( /44ALæ²5üùk<ŸæP@†f‰uè6ò»»ÎS#œÅi?YÝl­–ò ì«îÿ ù wú¡®¦ò-XoøéÒ•gz´É¤ƒžíd± RìANdg_ÝfK·í~1ÒËÑTB©BybGÇCçkää0ì™Ûa\èmT†«ÖÉN©³° ·ï:O FÿLNúÉÒº\á †4ÉWÜM¿ÛiÙÉN:ì|L–5H Ú`m&@ŠYo0lxæRÛ3÷Žy6 Å2H± *³ð¤è¶PÀ­n÷‹Š*SŒœ¤5Ò2—å—m§­´;åFçk|1BC œ¢é¬.FvøÃo9²àÍ©µÙ»ÎSNÜi2YZJ³æñN#yçk'Mò5>Yn®3À¢§›¬µùQw5!tœnŒ Å¿?'=Î »Î‰¢Ûæa^üb„×zƒ3ˆvÿ!M@MÆyëíÉ2da1b£MæE*ܵë<bq†ŽÉdiµûÅÕþ´šKŸhí3;¢AmBûò)q¤žôVõSÅ•çË :Ç­@n–¹s÷.–AŠ=¨/Ëò3¢Ûr 7±à#\mªi׿ Éev¬]Œˆn›¤‡Jê'ËÎPÞ>ƒ£Í²¾»áÅNþ¨»qРÁ8I*qÐÊÉ4Õ♦ڦyöÞ›qà¯P :Å´Æ‹ÝÖPµ€Íj‹‘±\Lse3ƒû£_’_pœ–_Õi×yj4VgpÁO–”6düâ¢/˜ö„pаÐ'ZûÌŽèFPie±ôîI÷û®ó”³ƒ_ô“å Ýø‰5t ¦ÆprF²<ªÜÅ2H±­ñ¦D·5T-`Ó/F( ýkÌw£!(‘«Å#¬Šl«+0°„Œ0q‚JëSàv§E8““~²4º…Q%Lò äj»ÃÉŽ#>Y^íb¤ØƒÖxe¢Ûª°é# ôb 6þpÒ*q¢£ý5²aq¼æŒ*9 \aüä`Äm¤„e"¿±i¸}×y*Äâ ~²4ºiTÉò„Q,ƒ{ÐrMúD·¾&»8ã#6>¿AÏik`O²|q3Öñ4¤дYÖwGÒ ;X¶Q%ÍøÌ;w—ýg=úûÆL^üpWÃÁï::ï:O9ñ:ü%?YRÚðCˆÄÐOr1g"“åáE&‚Å2H±­ñÖD·5T-`Ó/FØhÆÐÖYt Æàh·tÊ“Ébбâ¦Lb‘µÙÜLèÇÑÎ6Æ’KôÍa\ßg8³ë<bq†?Y:I.æÒd²Ü©à¤ö×ñóo×9QtHW{8å#ßøæ« Ç6Së”ôzp‰÷Þ9ßþê#ìl‹nQ1¤kôÏÕ1Ì*w³ÁÇó'xk敟֌ôl'1¨”éèSí%?Y¶ÓØ ·Ÿ,«Õd2`´´4­/Ÿ>ûè½O>øŸd™J~þí:'ª ™L{uzð‹«»1¤¤ÓYZ,—Ðr 1˜Ä–hæÓct£PÂ@$ÃD¶q]y­õ®óÔu$Îñ¿Ÿ,g­}ËQéÖìtj±§G’fÛæ÷ç©#ÛI@Å2H±9‘}It›-ݶ7úů•üÂAE)4BRÏjørêŒk}·_ŒÐüögí °Á]o¡‰ÒWo×yÊÚ±¯úɲŸÒ’ÎøÉr¿Âò´é’¾÷€¡ÅÔÚhö·Õ&Ë ÅäDvö%Ñm¶tÛÞ8YŒ4·O€ Õ4k“ä$½o~©2¯tƒšyo_º¸¼äðþv§¼ˆýÚd²ôžu^²Ü…ä$xˆÆ‚“dÏ×_ý:aöË Å´ÆkÝÖPµ€Í˜b¤?ñwVh𕟈“hÃóÅ3ãèP`F€µd6 ã»ÎSãÑ:þ•˜dé#̹ºF²¬ã•\/hê§e²É¡§_‰L uÂV,ƒ{ÙÙ—D·ÙÒm{cL1bm’¡e²ó5C1òò)̲ŠXãpGyÑ(Jb³žgú€©ºë<µmÂØöé1ÉÒIxþ¥£Ò|aÍ,ÔÚlmgŒVá ²ëœ(ºm[Ì~º_ŒðZ­ŠÄ']oýÏ,Å™‘\Iw£(ÇXtž=»À§­´L·Ý§ŸÌ×°7\ÿ®]ç©~tÎ猟,ýT7yõ¨tkF•Ü{ÀO>f¤Ú¡Q%Y²Œè–EÆòFüb¤½{©ý,ì|ú%Id1bóÈ›ô¬™ý1l6õ;¼Q×k†nÞ8æ™ó¢›#NÍ—üd駺ɫ‘ɲf}ÃF[g1SDÉ\ƒžíd± RìANdg_ÝfK·í~1Bç½lvÀ‘=÷†38ü’$²!KÒG`“µqø;R±iSh¢tÜužrâuøK~²ôSÝäÕÈd¹;‘›~®—ÇÍ@Ož~ŽF¥X)ö Ñ¨.¸ º-oË[ã‹›ûFÉ0Yzk# YÖð^+ɼ[¾Â#>;>Y†ÄïX#YÖð˜2ê°t[ÓÝF'‚ý­$ƒˆn5¤s C|1bt\·d¬`Y»a´3½ Î+ÛužrâuøKñÉr,í9ç×N–[½ëGøÊ—¾Ê€I²*‡žb¤ØƒœÈξ¤ºÛlé¶½1¾©…n­•4¿T2àyÛ—x¼§Ç'K‡bc—ŽM·Sì.Ø|ÝãæÍ -bZ#/ˆnk¨ZÀ¦_ŒðZØIëíâÓO򿃱ÄίQŒ滦\ਤá¥À›:«GøÉÒOu“W×H–5¼í›92÷G8šÙÜ/Ÿ’[°ƒN±9‘}It›-ݶ7úÅH™1“I €³€Wë\pnßužrâuøK~²œä—ïá¨t³TÁè,†fÑ INq•üüÛuNÝvZàøÅë“´GHöÝå‹[eˆ%dagQJ{»ÎS;MQY‚í'K?ÕM^=6ÝLzܨÊÙVŒÎ)–AŠ=ȉììK¢Ûlé¶½q² K”PhŒ¹ÇÊ“5Šêka$‰MsÜužrâuøK“Ér,ÉÅœ_#YVóF.艶…Jødü$?°Ë ÅäDvö%Ñm¶tÛÞè#¼Vëw³ÆÀþ§_˜d/Fl­’ßû©‰Ær%• Ûö%ïé~²ôSÝäÕìɲý!“@›õI˜šÊqê€cåI'xÅ SìANdg_ÝfK·í~1R[¿y„<Ëó Z%K ‡ðÈ‘E?YNòË÷pTº5«JÞ{`ûC‘/è€ã·]oÎ)br";û’è6[ºmoô‹úÝh }^wùb„<{qùñåó'ÍzË×+3Œi¸ë<5©s8ï'K?ÕM^=*ÝØšŠ ÛN¶"k ±î:'ŠníDµ#÷d1B_[ènë;ü’d²aE޶\Lakú Æÿ€¥6æGøñ¿]ç©ñhÿÊd²ôžu2YîT_&¸±y;ð6eFtkk2Ã-ºÍ­†[üb„× AúÝmáÌÂb„âmèJóÇ0óhúh~á á…àµoï¸E·Ž {ùê'K?ÕM^=*݈yçôãðjw³ÎdËŒï:'Šn{)7:áô‹‘µûݰO-, ñÂÑt‹7ëÿÿÙ¨’wÿíŸì²5¼prØ·öÓ¥úó~²œä—ïá¨t ë¯òÑÞ°ýtÞv1è{ÙÙ—D·ÙÒm{£_ŒðZa´ê[¿ƒ:BïÛÂb„u_É€a;*þÂÈô•˜ÓZ%lÖð"ºm›„ÖxºŸ,ýT7yõ¨tköøâ‹öë8¹ýí‡ÏbÐ)ö vôs¹E·\J¶3YŒÀ2d€£qY“¥Gð0YŒà¡ãgr !¡q’Q%Õ4¼Ü{à(¶ë<åÄëð—&“eHc3$w1Ùî°éw#k\Lh2Ëø_± RìAãqEt›¯Ý¦wÆ#l*Ú¬rÍ8ò ù(¦T™,F@g{x?JøSØÈ#M0î?´»šÙuLVÝôðá1É2&ú™L–;ÔúØ#ÀÆ\áàà× bÐ)ö '²³/‰n³¥ÛöÆøbÆÑ P¬G½‰áŽƒ¥G8¹F1Bc‹èÆìÿo×yÊÚ±¯Æ'ËØâk$Ë^»ÛÓ°O¦`¤–9yUIãÆ®s¢èVCòž¿¡ŽÅÂÁÞˆì…mt㓟ˆ~‘YŒÐÚyZóß‚qúÚ } Cç‡=‡›w§B,ÎÐá'K?ÕM^L–»“ýý÷ÿÙ\ÑÍaá' ;)–AŠ=ȉììK¢Ûlé¶½Ñ/FÖ3iq”ÔFNâàk_Š,ηGWâónßuž ±8C‡Ÿ,'ùå{8*Ý ½ôµ±÷ ~榢Šeb QËèÝ2ŠYÒ”_ŒÐ½Õß }&K1B¤ýÄFNÚÎ}è31žÃí»ÎS!gè𓥟ê&¯˜n–;¬oºéD8õ#8é§X)ö '²³/‰n³¥ÛöÆÍ‹’=Y’Ó>5 HÇs¸qPÆ]ç©ÁÉÉÍ“åuf.òi>ôMÓÇoB'.Å2H±9‘}It›-ݶ7úůÕ:Ýèq3GçÓÿó#Ù'9­ÇÅÄœ –ޤÉqLó̶77|ž1ÏœßužrâuøK~²ôSÝäÕ˜dy …5ßméËÝ–*¸Ñý~1R¦ß¦NjdL¦ã`¤ nG óŒ·Ï¢›£dÍ—üd9É/ßÃQéF/[gIfj¾Ûòt.º-×p ~1ÂUXcb¬U‚›OëwkÎß¹›¥¡!…FU8ší¶Ý ÚIžE·MÕò‡úÉÒOu“WJ7f0€4OËcŒ›Ï~I&u^G± RìANdg_ÝfK·í~1btc%F³jÖ»dÜl‘nÍ:ÿ/Þ :<¹ø÷àvMK¦ïy×yÊûÁ/úÉr’_¾‡£ÒÍVmµ6È–b¤ØƒÖÈ¢Ûª°é#F7êk” ]6²ÓÑË6=çôƒs"Ò å%ílùwhÜ…]ç©›hœŸËO–>¼&¯•nÄ‹vk¹i QÝmqöÝK¸¿1ºYÝm ºÁ&ëh³~4úRòá{?u´À'‰ ‘‘›µSîÜu˜(º9JÖ|ÉO–“üò=˜ný¤&Wm-–AŠ=hT-º­¡j›~1²6ݬ»ú ýæ„VÌwƒ_c·plãÆO~úùÇЭ†ÍǬóó𓥯ɫG¥¹‰£-øäüšbÐ)ö vôs¹E·\J¶ã#¼VÆx€J fvƒ>q3³¦™(zï_’D#üà$qÖØ¦6ö[ôº¯ü‚U†˜¸ê(¶ë<åÄëð—üd駺ɫ‘Ér"¿x·éÂîþiF@W‘Ôï¢[ªb•øŸ,FXø {S©ôQ(º9JÖ|©†dY³>ƒa ³S›åìØ(J+q Ê”~RtK׬Š;j+F Ýä¾6A8ºèª _ûÑ­¯É.ÎÔ–,w!š­»Åï½fN÷˧Œ¶â§`%?ÿvE·]¤ÿ~ +)F¬…!”8ú¼u¦53Žü{Z•ëÖõö—]ç©vDÎÍ]I²Ü™ìí‹/ø}h­÷MÖxù´3Τ£b¤Øƒ:ÌòUtË"cy#5#Ö×ÆÌ¦-åÎ]G*úÚ˜ïfëLâ¨ä§iùwì'Ö,w§0-“¬ËO>÷eâ8)br";û’è6[ºmo¬¡Xt¥1>„,Ù¬ÄŶf5¡á?®’g ¢Û°L;?[C²Ü„¶š¹ƒ¬Ä _ùÒWíçâ³/ÇâR :Å4Ó%çE·%êmxïæÅH˜pÊxø¢+ `1—mL“×ÿ@ðjQJw€å®óÔ˜çp~ód¹G‘™­C74}mÉK± RìANdg_ÝfK·í›#6A›6FèFÝFàÅç˜,ÔÚÂH’@Æ1Ï»ÎSc‘:‡ó›'Ë=ŠL ?G;ää#g?)|Ë ÅÔŽ~.·è–KÉÂvj(FسhŸdîýœ چ°TU<?;Ší:O9ñ:ü¥’åîDf™œ…ƒÝ+iºßuNÝv—,À5#ŒdnfèÜ{ÀÌqHG nLLòÞh hÉtþv§œxþR Érw"7ýn÷²7Ÿd(õLÝuNÝv—,À5#ÍŽ6>­3(œôÞÅ®óTˆÅ:jJ–»‘Ÿºd"F•ØÀãzšîwE·ÝäÛ­¢yùÔ–Õ²v•Û\ôm×yjQÌw~sÉro‚6Æ\Ù\$Ylœ¦{µL.¢Ûr 7±PC1ÒôÜ{À0H›`?;é;8Ík[¤Šè¶H¾ín®!Ynû™O†nL`·™ÄÁò¢ÛL5[·‰n-1ö䬡!Òãfª}çǶ02'™ °PJÑm¡€[Ý^C²Ü*K&¢¯ƒZFZl_ƒÅ2H±9‘}It›-ݶ7ÖPŒPq #™™… àк1„r¡8»ÎS ã¾ëÛkH–»°øÁ©Å2H±õ#¾üŒè¶\ÃM,ÔPŒPY #™­+tÃáˆ_NyLº]穱HÃù’å‘t¶©4ýË ÅÔãò3¢Ûr 7±PC1Ȭ %|2žÙ_ý5R«]ç©È8Ò[ ÉòHŠnKÞ¦è¶D½ ï­¡¡ƒ ~’V¢[’\õx®!YÖ£ÆòˆnK4Ý–¨·á½•# ;ई%º¥¨U‘ßJ’eEŠ, Šè¶D?Ñm‰zÞ[C1’´N’V¢[’\õx®!YÖ£ÆòˆnK4Ý–¨·á½›#´I2¤„™nŒ-‰Ù'I+Ñ-I®zýü˜¤É®óTRL湆d¹kI)ÉG_ùÒWé†s"R,ƒ{ÙÙ—D·ÙÒm{c Åõ5 F7“m)Irí:O%Åô`žkH–;•´iäùôòÅ»¶‡©¿ÑF± RìAk¼5Ñm U ØÜ¼± nï¿ÿÏÙ¦Üýµ™¤É®óTRLæyód¹S=™@/6=×ì$ÅÞÜ~DŠebòã;ïªè6O·ÍïÚ¼aâ6M(íôÃoÎ\²ì:Oåav6O–{í“~e-üŒ-á Ÿ¯ƒ›–†ØË Å¢–ÑÑ.2š•©µ¨¡a+·ŒDk+¶ë<Վȹ¹kH–»Óœ}íéÂnƸ0 ¹}ÉÜÅ2H±õã¸üŒè¶\ÃM,»ÙužÚ$=TòÐc'Ë•Dþ_ýϬe×1ÝœU[‹ebêD?ËWÑ-‹Œå»Ùuž*ŸêyⱓåJ:Sq£¯­RrýG¿tsVm-–AŠ=è:ê9ÿ‹n9Õ,hëØÅÈ®óTÁTPÝ£Ž,W’›á‘ŒÈb<‰Mex hco)çqÅ2H±9‘}It›-ݶ7»ÙužÚ6alûôc'Ëõ´¥ÿºLrÿ!³l:K—ôŸ[,ƒ{P?ŽËψnË5Üı‹‘]ç©MÒC%=v²\Sä‹ç¿ý «»5£%_¼ë?«X)ö ?¾ó®ŠnótÛü®c#»ÎS›§ pìd¹’°¤ö¦5’mNu7k¥dœó¸b¤ØƒœÈξ$ºÍ–nÛ]Œì:Om›0¶}ú±“åJÚ6cHî=`~m`Kùk’Ë Å´†¶¢Ûª°yìbd×yªÀÛ¯öÇN–+ÉÎŒ¶ h·p4# ­É ·è6C´n9v1"ºÕÆf„áØÉr† 1·|㛯²µ}Ûç³ÞÓ|·¶ óÜ¢Û<Ý6¿ëØÅˆè¶y›€c'ËyšLÞÅì´ôÖ…ynö“ºÿй±X)ö '²³/‰n³¥ÛöÆc#»ÎSÛ&ŒmŸ~ìd¹’¶Lmc:LJŸþÞÚè†sW,ƒ{ÙÙ—D·ÙÒm{㱋‘]ç©mƶO?v²,¦íÅåÇLpW,ƒ{ÙÙ—D·ÙÒm{㱋‘]ç©mƶO?v²\K[Öà Ëp½|Ê\€Éî‹ebZC[Ñm U Ø´X)ö Áh.<)º-p«Û]Œì:Om•$jxåJ C7–šÄ8Éþ‹Ï?cääýRýnËÕÝ–k¸‰…c#¢Û&‰jùC,—ë3hÁf0;U2(TêIÑ-U±Jü»Ý*If©Á8v²LU#Ò?u·?þó7éh ë*«î©žãMtsÄ©ùÒ±‹Ñ­æ´ç„íØÉÒ‰ø’K4K²>Éå‹wÃj“ì[JOœc³X)ö '²³/‰n³¥ÛöÆc#»ÎSÛ&ŒmŸ~ìd¹’¶o¼õ6,c%.6é`ú›óW,ƒ{ÙÙ—D·ÙÒm{㱋‘]ç©mƶO?v²\I[V•dއO´öÓ‹ebjÇ.—[tË¥da;Ç.Fv§ §„ªwìd¹’ÔŒäè×þnAf|ÝfˆVÃ-Ç.FD·ÒØŒ0;YÎ$æúÝšµ%[M¿›Ö*i 2Ï)ºÍÓmó»Ž]Œˆn›'°y8v²œ§Éä]Œ™lèÆ¨’çOì`õÑmR·I¢Û¤Duz8v1"ºÕ™ê&Cuìd9ýyÂþn , ‡è6OÌö]¢[[¹]Œˆn;JŠí ;Y¶cšÑmóÝRbcKøüß¿ý?¢Ûr…E·ånbáØÅˆè¶I¢ZþÐc'Ëåú ZøÎ9Ú—4ß­­Æl·è6[ºmoYÉœ¯šøv¯~N,E·ªÙ*Êt½5“Ýî?¼jŸÔ83¤¼}‹èv[}»¥…8£qÒênÖJ©4sK#}i) ºµÄˆuÚ7èÆÄ7-¡ºÄàd¾:÷«R{ÙÙ—TRÍ–în4¨S«¾ávœ<‡¸+Ž3Ýfˆј¸ÝŒœ¼ÿÑ’4NÚWÇT1è{ÙÙ—D·ÙÒþF›æF§1 ˜ã+€ÓķÿýyÝfèFžb¾¡ÍÿøŒZ›uÃ9¦ŠA§ØƒœÈξ$ºÍ–îð7Ú47[0ºY•¦ÐMßÿöçEPt›¡ÛßPe#[5IØÀÔºÞœîbÐ)ö ºMÞ"ºMJt¶CB^³èãU6Ü\:[YqGÑÍgìÒo½ý§ß[—ǘ΃N±9‘}It›-ÝYÝÈ47èÆ¨’³Šµ"›ª€è–ªþ!ˆ?w»o³tŠ=¨ÇågD·åžƒÑíÞòò8ŠnË5Œ±P :ÅëT?¢[ªbçé_t;Ï÷žkÑ-U±yþ‹A§Øƒæéàß%ºùúèª)Àj™Tb˜T@t›”(‹‡bÐ)ö ,²tŒˆnAôuP«»½ÿþ?^ÕI)` ˆneRB1è{к‰nk¨z<›¤ên¢ÛñÞlÞ‰nyõ³V :Å4Ó%çE·%êϽ¢Ûù¼ë%1Ý–¨o1è{P|Üã}ŠnñZ³OÑíœß~|ÜE·x­–ø,bZ¢ÆØ½¢Û˜2:ßVÀ覸ښÈÝW@tëk²Æ™bÐ)ö 5TÝÖPõx6)µÂZ%Ç‹b”KÑ-—’¾bÐ)ö ?¾ó®Šnót;·»D·s{ãóâ+ºÍÓ-õ®bÐ)ö Tbü‹n1*Éè¦4£€èÖQiÞ0ãÉ»ŠA§Øƒ:ºeù*ºe‘ñðFØ>X-“‡ËË#ÝV:HìLAy…ý½|ÚvQ„<)Ì‘w•)ºE·å™B*W@ÛºUþ‚¼   6ã7á¼»VŠ¾è¶’°2[¢[=ïB!Ù‹ó85ﮕ4ÝVVfëQ@t«ç](${Q`§æÝµ’&¢ÛJÂÊl= Ø>ÝšïVÏQHêW`§æÝµ’¢ÛJÂÊl= ˆnõ¼ …d/ ÌãÔ¼»VÒDt[IX™­G×^ûöŒþñz¯Hò ÌãÔ¼»VŠè¶’°2[¢[=ïB!Ù‹ó85ﮕ4ÝVVfëQ@t«ç](${Q`§æÝµ’&¢ÛJÂÊl= ˆnõ¼ …d/ ÌãÔ¼»VÒDt[IX™­GÑ­žw¡ìEyœšw×Jšˆn+ +³õ(ðú«_ר’z^‡B² æqjÞ]+ "º­$¬ÌÖ£€èVÏ»PHö¢Àýä㤌,º%Éåx.³ž]Ú…dg’ «(Çüd•) LT´Qp#Ù­’ËUwÛEù¬@.Q€º›~-P÷JHD·H¡&½©Èš”HP€“t"H ) ÖV@tË¥°è–KÉcÛÝŽý~»zÝr½ Ñ-—’¶CÛ»èvà÷«¨U¥€è–ëuˆn¹”<°g/^Šn~¿ŠZU ˆn¹^‡è–KÉÛyöìº1ÙíÀqTÔ¤@% ˆn¹^„è–KÉÛaT³èvà÷«¨U¥€è–ëuˆn¹”<°ÑíÀ/WQ«MÑ-×Ýr)y`;L,¥îF¦;p5)P‰¢[®!ºåRòÀvD·¿\E­6D·\oDtË¥äíXvSÝíÀ¯XQ«GÑ-×»Ýr)y`;Ue·묨I¨*»iI¥Éc+PUv;¶ÔЍ*»‰nJÇV ªìvl©;)PUvÝ” ­@UÙíØR+vR ªì&º)A[ª²Û±¥Vì¤@UÙMtS‚<¶Ue·cK­ØIª²›è¦ylªÊnÇ–Z±“Ue7ÑM òØ T•ÝŽ-µb'ªÊn¢›ä±¨*»[jÅN T•ÝD7%Èc+PUv;¶ÔЍ*»‰nJÇV ªìvl©;)PUvÝ” ­@UÙíØR+vR ªì&º)A[ª²Û±¥Vì¤@UÙMtS‚<¶Ue·cK­ØIª²›è¦ylªÊnÇ–Z±“Ue7ÑM 2U7ß|ÌñÃþUvÇ£G°ÉÆF?ð½ï²77ŸyÍVhMB¥¦dùÏ®€è–KReç\J&Ùk¿þÍ;k¯½öí¤Äx®*»ÅxžŸ]ÿLeÝU¡Ue·]g Ñm“ä½/º}ñùgÔIç›hUì¡»ÎÈÅTÒƒÖV@tË¥°è–KÉ$;û¢[RÔöëYtÛï»;RÈE·\oStË¥d’Ñ-I®2žE·2:ë)¾¢›¯OüUÑ-^«Œ>E·Œbæ2%ºåRRv–( º-Q¯}¯èÖV£˜[t+&uüƒD·x­äs=D·\ÚŠn¹”L²S Ý^>½äÐßIÑM ¡D·\oAtË¥d’Jèöú«_g[RÈìYt;ðËÝQÔD·\/KtË¥d’JèÆä¸ï¼ñ8)äö,ºøåî(j¢[®—%ºåR2ÉÎÆt{ñîoÿŸÿ‹ãÿþwþò?Ý1÷å‹w“¢p<Ï¢ÛñÞéc$ºåzk¢[.%“ìlK7²Ïø/ÿï—ÿò·wî?äÀÁñ¿Þž…ãyÝŽ÷N÷#Ñ-×[Ýr)™dg[º… Ò2©~· †è¤cCD·\â‹n¹”L²S ݾñÍWE·ðâD· …* ºå_tË¥d’JèFkä“‹O ù=‹n~¹;Ššè–ëe‰n¹”L²S Ý’Â|xÏ¢Ûá_ñ."(ºåzM¢[.%“ìˆnIr•ñ,º•ÑYOñÝ|}⯊nñZeô)ºe3—)Ñ-—’²³DÑm‰zí{E·¶ÅÜ¢[1©ã$ºÅk%Ÿë) ºåÒVtË¥d’Ñ-I®2žE·2:ë)¾¢›¯OüUÑ-^«Œ>E·Œbæ2%ºåRRv–( º-Q¯}¯èÖV£˜»ºQ˜·ãÛùÚ¾„»sµóµãy§_‰”²ÃNßÝ‘‚-ºåz›Êι”L²SÝþ¿ÿzçÙ³ Šô/>ÿ _($yî.Y™¸‚å$:/.?¾|þä‹~™wûÑÍIºTLÑ-—Ô¢[.%“ìÔ@·;wî>ü£fåW^ù¾:QHòÌâÌÿø·ö‡ÞzþÛ¿ÀÁWÇrséù“àÔúœÅ3¡Å&ކq½?ŒðèÎiN²^tçdû«èÖVCî­Ýr)/ºåR2ÉÎæt£$Xÿý¾ÏZ%,ZrçÞƒ±($yÆ–›ãÞƒf•f>]nâN}úÉÇöt¾ÿÄ÷`Ö‚æk?Ø¿þÍ;}#“–E·¾’:S^Ñ-—æ¢[.%“ìlN7š")ÿ©XQDZãïßþÙX’˜Âí’wÿíŸìÌïþõwxædð`£[ˆš9x„c™E·ŽŒúº‰¢[.ÙE·\J&ÙÙœn„ö?~õ?_U¯"jXñž­Šôì£÷®¹ ‰òÚ}ë³Ͻ~ú{ª`Wµ¼©€Ëgׯq Ò êÙ¶>üçošƒOÜ¢Û­ /U* ºåz-¢[.%“ìlN7«‘¨°\^^0NƒÚÖXÒ< {¦¨?ù֗ǘb•;jû´êØXH85ŒÛÀ•ÆøxƒjLjZ&;‚èk ˆn¹Þ‹è–KÉ$;›Ó­i¼÷ÀfÐuÅ)Æ¢ä#ôˆ…SS/›êwk?·¡Û8°šœv\}ã­·¹ ŸoüÃgíÛ·o™1®ìà¨KeÝré¬ìœKÉ$;›ÓÐ2˜4ØAÅÍaJªç¶ÔÈœZaÛgpû›òX¯™pyA§^üðò-‹nñbÊçz ˆn¹´Ýr)™d§ºAÊs'ÏPyFb¸Q˜ïy¬ßíæq/Ÿ2ð£Ûÿâ]rs©ïzù”ÐÒ8I}ó4¿;¤äö·‚í‡Dt»-¾m£€è–KwÑ-—’Ivj €`€âß¼ö'ÔÚh?¤•ÞÅb=Ï<ã„qÐ’ hÆBbþÓÇoÓø‰g¾Žù4ËñqÝ%u©˜¢[.©E·\J&ÙÙœnTÙ€Èÿþíÿá“ö4ñá`ÜÅ`,ÖóÌãlÈ m†t¨‘›™wãýt!$N ?'— ¢Û Œ:YXÑ-—à¢[.%“ìlN7@øéçWS§™kSèÒü[Ï3cF€áìß|õÑ£GœiBÒ›Âf3¶ê˜Í¬½1('û¢›‰¬ÏmÝré/ºåR2ÉÎæt{öâ%\°áúLs¦ ¸ŒEa=ÏöDBBÚk¯}ûõW¿~ÅÙ/€ðü/ïüÈ®âà딓‚AÑmPs,¬€è–KpÑ-—’Iv6§¡e´äÕ4êÓûS¿Ûh$ÖóÜËùÿÃgÖéFŸÚh8./¹ ÑX䄇ï9)Ø¢›#».S@tË%µè–KÉ$;5ÐÛèz>›~·ûý(Ä{Wž_¼Œ·ÌÓé}ãàÞë¿Q.gŠ\戙k‚m]{×–þ‹n¢èTqD·\’‹n¹”L²S Ýš,Öçþ„éc-¦ŒE%Ö3µ*ÖÈŒ÷ §Œ!‘có³oÄ&ÏüÁt牢›#Ž.S@tË%µè–KÉ$;UÐ-b+™›H¥x¦ÁŽ< ³·ÎÍ#n»®Ü>Ë(”÷~J'­ºŒƒ*u<Ù*Êv5|6S ÆGcbAtëȨ¯›( ºå’]tË¥d’è€hô·’ ‘Š÷ #€m†ÖÌØŒð_Y+Øï;ÆèFÿ`Ó]Ș_ü9út «(7=zwîZ3&Ö‚î?(œÝ‚rl¨€è–K|Ñ-—’Iv6§›M=l[»  Æ"É3œp„ºmƒ¶ðˆ³·Îàãìä Ýl?àÔ¾÷]ZSyÁ³Cý1 ;aiJäŒè6&£Î—T@tË¥¶è–KÉ$;›ÓÍFËÛJû„|l+‹T’gn"ÍèÇÓHH™$ÎÕCO;Ðun !aþtã*tãdÇ[øÊUÀm_ŒŽgÑ-è&dž ˆn¹ÄÝr)™dgsºZJ~ª6Íꎗ—“[ÉÄ{¶Š9Xðçï­ãˆ6Xwà !¡™”yß0ÔºÒœñ0°õ´TuÒ‹ŸÿÿƽÎ1®ìàè£KeÝré¬ìœKÉ$;›Ó­Á„Í2‹ØJ&ÉsÓZ½·Ž#Ú ÝðOK#zM§Ûý‡ÌhsŒØº^p°é7¼s×÷,º9JêR1D·\R‹n¹”L²³9Ý-²÷i+™áN7‹W’gÐGì`x °çÅ»—N'º97^‡àöÛ§Œ1t2Þ¾~óMt»ÑB®íÝri/ºåR2ÉΆtûäƒ_1­éq;Ít³OF€ ÎJKòÜRà‚qø<›8àKëÒãÔªš1!æ:<„„“æiðFÆ«°ìÿ• ×­´¨¸MtE§Š+ ºå’\tË¥d’ éfëêåK_"vখ4Ø'•ä9(ÑÌ`Óø9¾IM`ô£†Os †ãÐA˜Ì¡c9JÙÌÎ~ù4<·ïˆ ‰Ý(ºõÔ™ò ˆn¹4Ýr)™dgCºÑÆHˆ¬üñŸ¿inM? Ižíö0}€a̰ž¯±AûÖà ­èD³¤1º}çÇ 4í¸9XåœnÝú @gÊ+ ºåÒ\tË¥d’ éfá4±Nv9ÎG’ç0% º1á#Њ“ÁZßr´·™wƒ44bõÌ ¹wljHD·þ«Ñ™ò ˆn¹4Ýr)™dgsºÙÄ1ÉÓ¸Ç>2ôd…Þ®~DÒ<ŸfXc“t£9޾Ùp¡+ ‡,na£›é†Û71Ñ-¼96T@tË%¾è–KÉ$;›ÓÐÚhƦ¡ïÔ;œ($yfŒ"Ö8h™„q46:–¹dþé#ã²|uü³äˆU ñÓÌws·6H ‰èæÈ®KÅÝrI-ºåR2ÉN t#ÀÖ¾Ç'3èùò£ä™éÖ•'‡âc¹ÁЩ+íËù[?í«gÜËõ™á ñ!Ý®•Ôÿ-Ýr©/ºåR2ÉN%tk‡ÄÄÿ%yž4Û }¼¼À& ›Lzô@?3¼dð–þIÑ­¯‰Î”W@tË¥¹è–KÉ$;UÐùn×С™çü%yvì ]¢ “§[Ë$¤³ÅÁ†<Žžë,lBtNļ埓<âÖ©Û_D·ÛzèÛ6 ˆn¹tÝr)™d§ºÑÆ2†á…c±HòÌ´¡š¦ÂÆqZ{dÌ2çi´£Y\ëÞVÍr<^êÐÍöwëøœŒ£èÖQL_7Q@tË%»è–KÉ$;ÒA‰T”(É©1Ù Tsl¤b? IžÃíŒ$Á82m5g4æõ-7]fWƒüGöâ¹ößý?H7âÕ>ˆ,¡êÞÙú.ºµÄs3D·\Ò‹n¹”L²ãÓ ¦PC‡ÎgøJÝdì`À¼jIÔ>üô÷MEÉFKŽ™LòžË”48Âäk[êÄg wÑej‘l½=é?<(8:tcÒœU®i>yâfGt zʱ¡¢[.ñE·\J&ÙñéÆUJxçCç'éÆà bû´9Ôý($y·Sÿb¤"ŸœÁ19f’˜²b|RÑówÐOi;:tk_ nµL)ä¨YÑ-×ÛÝr)™d'‰n”öWõ¬Ó˜y*\Kè6Άnî‚üí»’<·os3 ¾†5h$†n“ÁVÝmP[,¬€è–KpÑ-—’Iv|ºñRšIÖ×Õ7FYГEC%“£9´ÅÔÝZá¼éíâ¤UµZW;Î$Ï{§¾¶dR[¤ºwºáÜŒßIxn‚ÔÐ-‚Î~E·qµu¥œ¢[.­E·\J&Ùñé¤XŽƒE9š®¢ëN1`G«½Tà&[&C8±C'W³Î˧c;àÌóîŠt)ÖËêx¦•’hvN6__>¥ âsœFø_1Î'×€Þ)Ñ­'‰Nl €è–KtÑ-—’Iv&éÖæc3^ücÚ'Cm®}µãާ[Àe³ΩžèD!ɳcgð’=‘œ@öºFv¼ÂŠ[í»šÕ·î=°–L|m_r‡*ž_ÔCâé\qD·\’‹n¹”L²3I7[Ù˜ÖHjX”óÔn M‹e¦~·˜pB¤’<‡»"Ĉ5µš¾ÅÓð†ñkÜ4Ãv,0#»ñsÚz€K6&““oá+£L­bøì£÷\~ëËáRß¡º[_)¯€è–KsÑ-—’Iv|ºñRnpF­êþCÆØ3ô‚¶Ê\ýn† º«B°ßý· îŽ#ÉsçÞ˜¯¡*jñSÕŒÏÖ’WflôH˜>`à Ã×þ³¬ÂK}ÝËãÛåp¯èÖPgÊ+ ºåÒ\tË¥d’Ÿno¾ùØŠbû¤ˆ¦.cÖÈö×Ȗɰ© ®—M»‹B’ç1#Îy"E}–IâK“£õ ±°3#Lúø¡Jkçmz€cß³-¡Ì”›ßÇ/„¾ŸpFt Rȱ¡¢[.ñE·\J&ÙñéÆÕ6Ýúî6Î:îHºÚ«Mmî?„žVÇq¢äÙ±3x©‰à½´LÆŒ 15 p¸}`ñ¸ADCt”E' + ºå\tË¥d’Ÿnô+1ZÒ9:Dk§¶ÉÚ|ZÕÆB’gßTç*M¯“3¾Ã-TÇ8Œƒ8âá,Œ9D·1et¾¤¢[.µE·\J&ÙñéÖ¦UÇMÿ#%:'Û_“èÖ PdFÀiR@Dø§=Û`˜S·¼´E?àMWà-O·¾L‡ä–÷¸/¢[œNòµ®¢[.}E·\J&ÙI¥›A7mÒÆYÇO7Vï·Ñ˜Øäð÷‰ôüƒï}—ÖBzÐèGcbZ¤&gL#ÓøvnŒ ‰caì’è6¦ŒÎ—T@tË¥¶è–KÉ$;‘t3¨Ùê‹ÖßÔ|Þ{Ð!Zûk<Ý`¥-ðE×°âD!ÒsC·ûB !&³­}hÚñI:|啯ÑK`ˆ£3È?2$N\Æ.‰ncÊè|ID·\j‹n¹”L²3I7F5ˆsµÓ€vÊzåÚ8ë¸#éfCë—È_0Zï™î3ëãÓ¦°A.ªrŽ8fÜ–q¦ûïÓÏ?Îç‰c—D·1et¾¤¢[.µE·\J&ÙñéÆK¹Úiú6Pã #;,뤛ͣétkùß8 NƇdÌ‚s^tsÄÑ¥b ˆn¹¤Ýr)™dǧWÝXi „Q½²3}œuÎDÒ’èÐrH{ Æ r‚Ëp$â=?zôˆÊ`Xl„O†5R1ôÿÀU<@-Aó’1 ÎyÑÍG—Š) ºå’ZtË¥d’ŸnMÝ­µG>MþÔÝ Qg¯‘t#´´"ÚS°Pü‰c‘ž¯¸|ïÖhœl †ôä±fL[:Òj©ŽïÈ8Æ.‰ncÊè|ID·\j‹n¹”L²ãÓ ÜÐZØŒ¯hm`u70Gs_‡hí¯ñt#ÀÔªlÊÈ$:-¼?›ϬF°ƒ©+S/Þíœà^Ÿ³0¨’‹Ë;ìë­6ÒA½“¢[OØ@Ñ-—è¢[.%“ìLÒ-Ëfv7˜;*±ÏpµïH¢[?Ì6Æ£~ð̸çfÎ}y€’£ÙggèÏ–ÿj†GÂ8ŽÓÌ;æPWò~uŽÖËf+kÏà~Ð3£7±“61á‹/”ÅÔÉ’ ˆn¹ÔVvÎ¥d’xº~1±iµ;a.œì;j ÄapˆmUC×Û‰Vs´­‘æV¶àÁŸæ€àŽ˜Æwóo-«ƒžC)>!lkoAïÍIÕÝF¥Ñ…‚ ˆn¹ÄÝr)™d'’nô² ûÄËpƒ¹>Ô™ÍéfÃ/moö 1ÖqPÚ!9ˆé}ã˜ìw³iqÏÆÇAË´‘"ÆizÅ8¤Ò‡†ƒží¤è戣KÅÝrI-ºåR2ÉÎ$Ý ýkÌw£ê« :6§› Ú·ikð‚fIè†ÃÑähó`ßšæÊ¡?C'ÆÃű{NO Æ x­Òç„„KÊAX9¶R@tË¥¼²s.%“ìLÒ­iÖku´µÝV‰D'7§Õ%BËTt>YS @3¥Î§ñy]«ÂÁWöܼ%~/~p°˜…6OjpÔõÍÚIÑÍG—Š) ºå’ZtË¥d’ŸnVÂSÈÓËÆBÚÙšå­N¼£*7†¶è†Wtžì‹C‰š AÁ³?f’öƦ»mjãžï¼ñØê¼øäš1ý0pUt›”H ( ºåYtË¥d’ŸnÔ¿ m´§µAã p´[¶Ï·Ý ënÖ®dzu¨ñ Và‹oo àN(â¾g®ãtêQ#ôßló}ÚXÏ‘¢[¤Pò¶ª¢[.yE·\J&ÙñéFSƒSml™Û¶å­õ/Ù™…t£„§æ5¼ág̳]5#›ÑûƬIì&ÐL¸þƒàã§Ž¼Ð NßrTe‡[ŠèË ˆn¹TWvÎ¥d’ŸnVw£ÇŠ‘‡mŠ…Ë%t£œofÝþã$ ƒ·ÏÝþÖ"žý…‘¹ÊT8»Ÿ c-“óBÂ˦C­ù› >šE/Ÿ?abbrLì­#ºdÕǶ ˆn¹ôÝr)™dǧ/ÅZ $!)ÒÁ„µX¶‘×qOÖÝð‘Nh6FóÉ£ñcîIÏØgT‰õ£Ùð’ÎãìkjH`“)À§ÑŠGŒMÁ¦9ið´Þ=:Nªî6¦ŒÎ—T@tË¥¶è–KÉ$;>Ý(ömÚ˜Õ4k“ä Åu‡hí¯‘t£úÖ>GyĈÚ"1Š}®â”p—U!øZàéƒ)Í\³Lí^£[;ñ‘p #i5Å8h…qH×4~ý%í­ƒÑmHE+­€è–KqÑ-—’Iv&év¸{ÂÀŠt smÜ5I7&” šÃ܃ âÙv­£>Nƒ›Ï`™Ô×ðo=bNE/)$A^"ˆzöÕfÕ…VÐàçêê'ÍVq4NžB{ÑpÓß"ºuÔ×MÝrÉ.ºåR2ÉN ݬM2´Lv¾¶ëkm÷$ÝÃ9Æ 0ÒÃöi®íp’¦?hB5ŽyîŸ IðÉ›@7›*.õàÌ3©*úìú~ÂÑ-H!dž ˆn¹ÄÝr)™dǧ/Åú•ø¤Ï¨ÿÙÆYÇ=n °¢ä{¶ ×ít3×,¨çÇ›­ÁeþmU®poßÁ£iô**uÕS%®ïëêŒè6*.T@tË%v»ÊeSv&ðéÆUÊaçè­ý5nŒ<¼ `cÐU³IÏå4¿±<)EËÕÿÈ`Òµ‡ý·L 8©xúóÄE·Õtª¸¢[.ÉE·\J&ÙñéFç ;šÞ®Ó¢%á Ž6Î:|ʘC«ž¦x$¢ägЦÕ"éVc…+/¾'ËVýœ´|²s7 L„ÿÏôµ…A’´yò'Ø¢›#Ž.S@tË%µè–KÉ$;>ÝÚÀ‚VÐíÑ£G퓎;žn¶C Õ:ÂW'  `àÙÂCŽg³lí4xú–±cþ â“þ“MÄóŒÍfÀ½täÑéæõijèvõJôoSD·\ò‹n¹”L²ãÓ—bV|†Õ9¬ƒÌz¾Ú•µŽ;¶îv⿼ó# 6ø:4ë³btbÓùü ÓÇèw£ÔñÙþŠ)Z2íŒMýmm7Å?`¸>u7Cç˜ÿ$ÏÁr¨ŸŠnmÙå®VÑ-׫Ýr)™dǧW)–£C´ö×HºZ:Ñx-8p Ž'Ä2uZöø4Ç•ÛíÀ²àÆ,·å²àÁ8†Í´¯vÜñž­¢1Ídd0gÇZû«ênm5äÞJÑ-—ò¢[.%“ìøt£®=B²ïnã¬ãާ¥=CDè™âÀ1~üЬ×9š®1wrá°ý80âÿwիɽ؂gæ‰C:{ʘ}F¶„ºÛ˜Ÿp^t Rȱ¡¢[.ñE·\J&Ùñé°le#ט»Ã5ûO·S€/šíiž?¹¸ì÷¸yœeÖ4'¶ö¦¹¹ðäÍå4Wó”ö_kó‚öé–;6Ž¢[K497S@tË%½è–KÉ$;>Ýx)Öïf}mýÏA®%Óíùë×£g G³Y̲?r%¦Ú«"ÿჷ×ÑôÜåû£5Œ¨t¶×¹z`JE·|oI–æ+ ºÍ×îö¢Ûm= }óéÆUëºûÌB7ˆÆTqš9hä«y¦Œ5økf|7Ž¡êX3­À–Y¶Nø%—›‡FaC¦¼1é›ÃÙ^Çü'ÅQt "˱¡¢[.ñE·\J&ÙñéF¿=Jáxýñ;îåt Ç,Ø6žpl ~ìV(b´±ê˜u±á“î3Ü´aÒùå‹i܌Р‰}pŒY>qO£è6¦¤Î—T@tË¥¶è–KÉ$;>Ý€}m¡»­ïXN7[ë8TÁž}ô˜pèÆd4<|çÇMíT5‹¯Í¤Ãേ Ú'¯ÝÃÿãsäö:øL£è6üzt¶¬¢[.½E·\J&ÙñéÆK¡I­ßÝÎ,§¡R´î¶¦‰ÏÉÐDF?R³!‹4fŽÅ˜"ü‘‡Ø‰7nÕ1²¿=½©rºÁNŠ£è6öNu¾¤¢[.µE·\J&ÙñéÆU«}.§%y³>óý‡¬ØLÈaðJŠ‚ãZÙ"““‡Ž‘ÁKIÛë¤ÆQtÔ\' + ºå\tË¥d’Ÿn¼”À5V±~70zß–ÓÐZ¿Ÿü ŽðOŠÑ˜g‘‘›<…j£?Ç­’^'öAPvh«'÷& ˆn¹dWvÎ¥d’ŸnÀ –]í}sç.­|Œ«wˆÖ¾”2ßí4òóϬ ±YK?Ûßz–£‚Èr^LhzO³ùì“î?ZbûUwsÄÑ¥b ˆn¹¤Ýr)™dg’n«Y–êñã(™Y³± ²Aw<ÝÌ m(ÃàÃÓ0H¯jÁõ,_‡á*œ§‰oa¶¡/,F¤ìˆY@Lt»–Wÿ·T@tË¥¾è–KÉ$;1t3xQPÓ I¿˜µU2üƒ‡¹f'#éfÃ3ŒïþÛ?Ñ2‰}g±ýøØ­gÙÂ`ÓÃÐR«E~Øh¤õ’ƒv]f˜›&MŠèÖWRgÊ+ ºåÒ\tË¥d’ŸnÔÑ(´ÃAõŠ%úCO•‘åt³†D”ê4KbGR,=›eV1ƒ-Ûãl›6 ÎD Ódl/‹! ádŒep÷¢[_)¯€è–KsÑ-—’Iv|ºq5°lбœnÔn° @ù¤6D%ˆIgIQó¼že{"cThG¥ÖÉ*Ê8¨ØŽ†äÅKbÇ t£®Ço†Sëë˜wíï6ªŒ.”T@tË¥¶è–KÉ$;>ÝRÒß }f9Ý­m`óøôKþ¤Ø€Ö°lÁ ‘ıiÔ[Lu·HUåmUD·\òŠn¹”L²ãÓÍ×ä¥È~7 -å9¤à g %EÁ÷lf×°ì?·5„„¾Eêz}áŒè¤cCD·\â‹n¹”L²ãÓ—bnô¸…Þ·¶Ãa\<ݚŲZ‹!Cá?ü´¼™q-0L5îñMjdöËIúàXº°þí¢[_)¯€è–KsÑ-—’Iv|ºqu°»-œÌB7Ö.¶í³måÿ±ðÿà{ßÅg{î˜ óÏyæš…%+ýMj0Îôö¦9tMÇxÒ¥†žÏŸ0ýŽE›6èÜ.º9âèR1D·\R‹n¹”L²ãÓíÍ77 »÷€•¬lˆ l·åñ¹´n6¨;ôµY±Ï„î±Ùܘ0w G3wÌW©iƬ3 Ýn:ÅâzÇ’t&‚tbP”l&V0.´©Ä ÿ‰núèlYD·\z‹n¹”L²C7f@±¦ü¿s—öFÜa üBº1ó‹6LÅ¾Íæl¸# §¤ÓŠOH£™8æÒÍ(ŒÍÉu&1nM©lúƒƒÖQÛ‹'IÌ1϶[d€¥MRpæô‰ncJê|ID·\j‹n¹”L²C7êkPìÑ£G ²d¤›š sÛêÄ¡9±ƒhܳнugw 7Ðfy6ý­m°ít£†h˜ËH·«9}ýòÄ⋦ÍÓ ¶èÖ~5ro¥€è–KyÑ-—’Ivbèfu·õè1ݘ6V#üQñ¡–ÇA%ˆ˜ŽAaçîÏ?Ã9ÔÁ?mcâPeóM¸£”Mº™•6’1#Îyp”1HW ŸMâøŸè6®®”S@tË¥µè–KÉ$;5ÐF&X°!)͉ƒQ z`Ä`Í®KÒ&5P’Zø³O#‘3A»ÿ8ÿ „È4rX{©ã_tsÄÑ¥b ˆn¹¤Ýr)™dǧ/…ŸÒ˜ÖHºº(ùù¼j™<ÍMÆ=vXû^R`ð éèPü£òeµ¶[õµÞö½ø³ÖöÜfÙHĽP&\Êèà)>7E·ŒjËÔlD·ÙÒunÝ:‚”ùêÓ r±ÒTŸ_œ¤)ƒæ;ó÷=p&žn·!rÁ°üÁ¸Ó#Æ´»“gVãoºÞ¨d zžyòåSX2²‘ÆIc3Þo;‰·Ÿ2lóC›g$I7\³-ÎøŸè6®®”S@tË¥µè–KÉ$;“t$W8 qlÌI8ñt£¨%¿3–ƒ Â2¸} @¸3v¡›íÂCÅŠöC*­|“î¿Nìü!+!œÍL‡—O‰)cü`‹nƒ²ëdaD·\‚‹n¹”L²³n¼µÙt£úÃb ”äW%ÿiOg‚6Ael†ÍÀAËa³x£;# I …BµÔîÂñ€Ý¶©«–4Š^ïIêÛn¤Q43A¹8ü`‹nmµåÞJÑ-—ò¢[.%“ì,¤Û’ºC%)ä›ÊÚ© Ïfg;´Á ÀÑ`È]ÔÚšõ»./a\R|Ï6+­ÕyÁƒ¨”õoi8{çn°98Ó÷Î4³î= GN7§~jþE· ›* ºå_tË¥d’ éFQOEŒÐâ‡UÍ£@_3”ñÞüXUdÐÖ7èyÆI«E2%Áîµ¹ ·F°\¥îÆHÆ«„`[Eïúz÷¿YµBÑ­+¾W©€è–뵈n¹”L²³œn6.t·G§g*2TNÉßÌ8U0eÃùJ\¤åoô¸Q£Ðf¥lüÎ9Ú›~·ñÉtÖ+Ç/v ÷«œª»µµ•{+D·\Ê‹n¹”L²³œn³ûÝÃÉ$µ1¦0D“޶ö]MEÏ]ô£í9Òm›ÍQ7ì<«};µ¶ÎDxD=®í§ãf˜J¨»u.õ¿Šn}Mt¦¼¢[.ÍE·\J&Ù©…n/Ÿ6#LXH?±¥q°å0I®çøt¦Úu¾ví&|ÝÄ’×ÕÝrI+ºåR2ÉN$ÝhU³&Çàà+nêSTµBkdÛß2 Ôš56#8-EÒŸJ–£%ž“BB˜™îÇ47¸L _—<º}¯èÖVCî­Ýr)/ºåR2ÉŽO7^ £íSÌÚŽ6Î:îxºaŸ–@šiÜÃÁפ(dôœÃ1Ým´Ž6Ÿî˜É¤@ŠnIrÉóJ ˆn¹„Ýr)™dǧW¯ÊðSŪïî­ý5’n6ß–D&äÍZÇwîF®$™ÓIÏ©!¡²FßM£tÃѳ6ùˆH¢[¤Pò¶ª¢[.yE·\J&ÙñéÆKaèûàa¤kã¬ãN¢ÛMwÛ˧Xœe–¯ž¯æ»…Ž?7$6 ÒæÜžuAû䌇Þ"º Ê¢“…Ýr .ºåR2ÉŽO·°ÂWzÜrÑÐҬ׬yV^¼kka%E!—g˜’<ÓH³ñwjE·TÅä D·\ªŠn¹”L²SÝš©Ð÷ÐÐÇ#~ä|RLc<7S ¢C‚gZ#í »ÜÇ<"Æè£’ü¬­€è–KaÑ-—’Ivj †Lpæ8Ícýÿ¿fÉÇÐlxu}tCûãN€VHâî9Í.wæÇÅZ¹ö'º]+¡ÿ[* ºåR_tË¥d’èÆÂ M§ÕiEbc˰VÉO¾õeÁnꀭm¨H:›Ú°ÈdCa;˜ìvšï6V匋RtóõÑÕ2 ˆn¹tÝr)™dg’nW+ó÷æ»Q˜·YÖwGÒÍvUûôó›fØæÆÂIÁ8¼×sÔÅfXKoGg/ XÙÁõl¾½uX`‘̨çMyݦÒõ ˆn¹TÝr)™dǧµ¶fã m ´ÖȦªr:C»\jáL$Ý Ö`(Í}n°‘ÐbDe*á+Tâ..?¦¶EŸ]R|=C(ðÔ9šZ¤; ’Òh 6ž³ ƒ¢ÛàkÒÉ ˆn¹Ýr)™dǧ¸ì·n“¤¦æì|è‰ P ŽHºZPÕ´òÝHuŒcŒ)ÕJµ‘ƒU³Œ¼Iñ÷ììÅcFЄþGs7ÓôòíV ºÅ¿&ù\OÑ-—¶¢[.%“ìøt³í³áT`Vph(ÞÙŽ#žnØÚ÷ø¤sJâunM €&\²áúöiX¼öœù?á)¾ÑPY#$§é‰n¾ìºZFÑ-—΢[.%“ìøtc¼{ôèQ‡\|µzo­ÉÎ$Ñ­f°Â­”TÐÚçJnN>Òñssi¾ëÂæ&ÄLò&Ñ-V)ù[SÑ-—º¢[.%“ìøt³ºm€HºÉòÖÝúa¾Zõ±uá_ÞùÇéD³¨£õÁŒ–—¥Nºö¬ÁvsðÕ±˜äÙ±Ó¿$ºõ5Ñ™ò ˆn¹4Ýr)™dǧ F1Ê|ˆÆüe>m5çé#«¸q~vÝÍÂß§C8¨K2¹ŒúZªû›–ɬtÃr³¦Öý‡lZ‡ƒX;b&yvìô/‰n}Mt¦¼¢[.ÍE·\J&Ùñé¤ÂÃÜÍç½N§Ût#¨5ºÃ õJb:Ù5¯†ñ”Ž?Æ‚RWõ÷âIòó)º¥*&ÿk( ºåRUtË¥d’IºÁ)PBuæjòò©ÒD½ÆGÛtcT t³e»¨B^Õ VÐJàÆ³áìòòn;›ˆÅn|´\×+™DynÝåÝ¢d’§•Ýr ,ºåR2ÉN Ý@¾ ´3þgö–IF›0ÅIÎÞ¹k­”ôŽ%Å×÷Œeºó°LÝ>>¾:þ“<;vú—D·¾&:S^Ñ-—æ¢[.%“ìÄе‚™ÏE7S8¨:uÆ™ôI—n!^¶$WScÉØ-<šÐh!12a!ÄeìÆÉó¢Û¤DòP@Ñ-—È¢[.%“ìøtƒPàŒ!l£ wÓ$xêóâ­u.…¯“tc64D $ÇÛÍØ !ξ3¶–c»°ÌÙÌwP’6HÒ¢Æ>Ô 1ÎØÈ1#\eçéê€cnN¢ÀØ-‘çE·H¡ämUD·\òŠn¹”L²ãÓÍÖ*éïZ,y“t3ãMeí´Q;ÎØqÖ@$˜<zÚl ¿ ¶çI· òg/ €q¶ ÓŽ¼=ûè=;I%—èàæ$ât|¦~ÝR“ÿ5Ýr©*ºåR2ÉŽO·f›ÎSA’¡RFg¼³óKZ&éVc0¡mo=Ó DY~|ÐÉÕ®©](Ó®j¶­ÄB«#Ãf Xczr•–L«Ò†ùæ›i–$ð” c·DžÝ"…’·UÝrÉ+ºåR2ÉŽO7ˆÞ‡FHÛ¤†bœÞ·€¼¾c²î6N§ßÍšIáH¨¬ Â0hgÞÉ«î<朎S¿Û¨%®Rël69µMºOŸþ$‚Q[·/ˆn·õзmÝré.ºåR2ÉÎ$Ýh{4ÀÝ¢Û½>Ú€Ý<ºQÕ:O¶ÀÆgî1“7{ñP£$⎘\%HíŠg®ÀˆnŽìºTLÑ-—Ô¢[.%“ìLÒ NÑÛÅð‰Ð‰ÛºÛ™~Å-nͲÿͰ×OGÃÏ¥þº[ÏŸŒúŸq¡c¿óõ¶ÁS—œvÀ¹-оHÑ-×ËÝr)™d'†nx5º¥èr¢i®s©ý5¾îf[ÉXG6›ñ“CŒÜ SŒ¾-–Éâ“@¿ñpÈŒwkØÄ2œ%޾qí€ãI©kûW@tËõE·\J&Ù‰§›A ¾Pæ‰ølã¬ãާ dD½µòÙ,3šæú±¸š|gë9Ÿsn:¼F† ôo9s/Ì^?Ź˂Ï×ÿ˜`ûÛå8vú—Ô2Ù×DgÊ+ ºåÒ\tË¥d’Iº5P;­ÈAé Ö8î?ô»Þ¢év5ÐfÁ¶Q%aÜH;.´ˆ2©œ3Œääôs•¶Ÿ…nÀÍ#hräÀ83µÇ Ú8ô2r‰ðL,ƒ=fÁ9/º9âèR1D·\R‹n¹”L²ãÓ ®õ¡F1ΰIŠôNe­óu’nL£õÐR»ÞtûÒùÖÝðcÞ¨Äqœ¢yÁ*XIñu<Û|·0…©M?àÈŸyÆÑÍf‚‡á.#7ÅžÝb•’¿5Ýr©+ºåR2ÉŽO7®6Õ´ÓAMÍvÀ±¯–õ¿NÒÍV ¬ÆßtxýíŸYW_£@} oàŒ&A˜‹Ãæ¦ zžwÎb˓ƞñFh‘ì"˼‡öïÝúšèLyD·\š‹n¹”L²ãÓÊnš4Ëaên}œuÎLÒ¼ÓtœY'×õ³¦ÃkM¿Û½´†ƒfÉ1ÏI"Ï4ÆÇsø¸írÂ#b¢[ŒJò³¶¢[.…E·\J&ÙñéFóc³–ãõ Jòp°z í–¢µ¿NÒpZWûÓéí k›„r#3àÂ×¼Œ[7Ÿc–Ð|Bº\Í’5ø™…n˜êÏäŸ-QÒöÖ0åÞƒö™bn[«„ʦ=‘ðg½©º[±÷¨9 ˆnŽ8I—D·$¹ryŽ¥›MLë}æ¢[Lt¨µqtþ¨ÇuÎûÊ̸PwËûPÑ-¯ž²6OÑmžný»D·¾&ÎÄÐ~1[¥¤ÿY’nŒ!±•¸Z²x‹eµ¼íÌ)ºíì…4¸¢[®+ºåR2ÉN ÝÖU`fØJ\¶RŸL1Ûªß-2Ìó¼‰nótÓ]yÝré)ºåR2ÉN ݨ»9u´±Kñ£J"LÍÑúþâËŠ´\›7Ñ­¶7ržáÝr½wÑ-—’Iv|º1j‚‘Š •ì–´3chã|vº/Fn0‘œ1œ¶z?øšß]xÝvñšHÑ-×+Ýr)™dǧ[{åUÇLÆ„™N7ºÞXÂò •¤Y’…¹b[ƯÊèVÕë8ÛÀˆn¹^½è–KÉ$;Qtë •dÂõõ™’u7úÝØžæùoÿ‚Š[˜÷Mï[R|wáYtÛÅk:| E·\¯XtË¥d’ŸnŒxï“lŸ)I7ÛçjÂ֎9ƒ ›$‰P›gÑ­¶7ržáÝr½wÑ-—’Iv|ºx±ÙM»ëªS¸4æÈÞïf¨¥)²}4ËNîOt;Ü+Ýe„D·\¯MtË¥d’Iº2Ú;nŒÌgÓ1®Ùùº mjsIÛã`¨»q\^²ƒÀÕñì£÷É ç]ŸÝvýúxÑ-׫Ýr)™dg’n P¼B}mÖx½Åµ9p1tKÚÔ´Á2Âm9pXؒ⻠ϢÛ.^Óá)ºåzÅ¢[.%“ìøtƒ_†6–/¦» –Ñ<øÆ[§ÝºïÜ…5 é°ÆmjƒgÖX¶I¬´Œ£<¹Ñ*ÊI"§zÝR“ÿ5Ýr©*ºåR2ÉŽO·o|óUðÁg‡babµÓSwãéØÜÔ†Š—/ŸRq£]l»{i’ÎIžE·$¹äy%D·\Šn¹”L²ãÓ5¸ •¦Ýøj›²ñÖú—ìL ݰÜÙÔ†ºáXu ÏìzÃZ%TâØ.ÇêOŠï.<‹n»xM‡¤è–ë‹n¹”L²ãÓÍênýu&B ƒ8Ò­¿©MSÙÔ†Šm¡Lpûâóψ#ŽŸ|ëËc(L¡6Ï¢[moä<Ã#ºåzï¢[.%“ìøt{ýñbp¢qÐIgÜÕP“{Æ*nœ©»õ7µ¡`ÛÔ†óÖ×fÜpÛ‘ß]xÝvñšHÑ-×+Ýr)™dǧjÆIžªiԛ찯|¾…t» jgÎZçkR|áYt;ÄkÜ}$D·\¯PtË¥d’Iº1€„N®fé­ë‰ãÞF-:h‹¬»YP&µB[bëÿöÏøš…ãyÝŽ÷N÷#Ñ-×[Ýr)™dg’n†0Îì`Fc?|´%Ñ튛6Ÿî„Ѥ(ϳèv¼wºÇ‰n¹Þšè–KÉ$;>ݬ—úT{®¶Ûa\L¿›•Ê3×l}-zâ™…ãyÝŽ÷N÷#Ñ-×[Ýr)™dǧWo5H¶'Oîåt³9k¬©uì Û»íúë9þÝÎñ­×gÑ-×;Ýr)™dǧ[g·¦®µ8?îåt#´ôâ1A›þäŸôRD·$¹äy%D·\Šn¹”L²ãÓ x1Û: )a…[œ„ÏÉ®·ø–I› Mƒ¤MÓ¦¶˜…ãyÝŽ÷N÷#Ñ-×[Ýr)™dg’nŽ!% ãNM‘ô‘ÑïæTÙÂ¥xºµLï}pí3gèÝÎð¥WeÑ-×KÝr)™d'†n¬fÉâë¹oÌæf¨IÙ #žn˜bF@R°íYt;öûÝKìD·\oJtË¥d’Ÿn´@†“æ°•'­Ç&ƒ\³“1tûäƒ_1Í ›¯¿úuv°NrRŽçYt;Þ;ÝcŒD·\oMtË¥d’Ÿn\]uÌ$ëL6zvØx•Ó“¤(ϳèv¼wºÇ‰n¹Þšè–KÉ$;>Ýš1“·Iv¾.¬»QŒÓÑÆZÍÃæ»±˜$<µ•$“"r$Ï¢Û‘Þæ~ã"ºåzw¢[.%“ìøt3xµ‡G޹û˜‹i™´ â“­mÌM[%t£xOŠÅÁ<‹n{¡;Žè–ëʼnn¹”L²C·>¹bÎÄÓ¥½–iÁfÒÕä(ϳèv¼wºÇ‰n¹Þšè–KÉ$;5ЇÊ#3©&EáxžE·ã½Ó=ÆHtËõÖD·\J&Ù©„n———ÏŸ4ÇÙ˜äõ‰nIiXžWR@tË%¬è–KÉ$;5Ðí‹~É:æ‰ÛÁפ(ϳèv¼wºÇ‰n¹Þšè–KÉ$;5ÐE&mKvØÁé’¢p<Ï¢ÛñÞéc$ºåzk¢[.%“ìlN·gÏ.$ÉÆ7Lèfòø‡Ÿþ¾™ðì")ó,ºì…î4:¢[®'ºåR2ÉÎæt³pèwcÖ#'¿øü3ÑMtKJÃò¼’¢[.aE·\J&ÙÙœn„–µJØû†õ¸¨»ýË;?âkRŽçYt;Þ;ÝcŒD·\oMtË¥d’èÆ8Íb\wî2ÆÑû–…ãyÝŽ÷N÷#Ñ-×[Ýr)™d§º`úÝÞø‡Ï..?†t,Éuæ¢Û™'€J¢/ºåz¢[.%“ìTB7vÀaÇÚ'ÿðÁ[Iá?¤gÑí¯uw‘Ýr½2Ñ-—’Ivj {ëÐ,6æ6À±#L’ârÏ¢Ûa^å®#"ºåz}¢[.%“ìÔ@7ÐÆR“§`_0ß©¸­.).‡ñ,ºæUî:"¢[®×'ºåR2ÉN%t{öÑ{l6 `jnèÆʤ¸Ƴèv˜W¹ëˆˆn¹^Ÿè–KÉ$;5Ð œ±\ pÑ&Éb\l*G³$t#s%Åå0žE·Ã¼Ê]GDtËõúD·\J&Ù©n_ùÒW›y×Ç9Ï Ý’Ò°<¯¤€è–KXÑ-—’Ivj º1€]¹ÃÁsþÝÎùí×wÑ-×»Ýr)™d§º&¸iœ›÷&ºÝh!×v ˆn¹´Ýr)™d§ºuvÀùäƒ_%EáxžE·ã½Ó=ÆHtËõÖD·\J&Ù©naœ×ÿX;àðúD·¤4,Ï+) ºåVtË¥d’Íéf;à<{ñ’‘“L ÇÑ’l‹ƒyÝöBwÑ-׋Ýr)™dgsºµwÀnìÐ Ø%Åâ`žE·ƒ½ÐFGtËõâD·\J&ÙÙœn ήwÀa·fœ;w“¢p<Ï¢ÛñÞéc$ºåzk¢[.%“ìlN7B«p:¯Lt뢯›( ºå’]tË¥d’èF€m¦hÔÝ’Ò°<¯¤€è–KXÑ-—’Ivª Û‹wC˜Ù€e¸Â×ótˆnçùÞk‹µè–ëˆn¹”L²SÝX‰ë´ŠòÅååõ»‰nIiXžWR@tË%¬è–KÉ$;5Ð œ1åíá]¢›è–”†åy%D·\Šn¹”L²SÝ˜Ä Ñþûÿ|ÿøŒOÑMtKJÃò¼’¢[.aE·\J&Ù©nÔÚÂŽ×Óß’"q4Ï¢ÛÑÞè>ã#ºåzo¢[.%“ìlN7[«äÃ÷~jÁþðÓß³WRŽçYt;Þ;ÝcŒD·\oMtË¥d’ÍéFIÎlî7Þz;û´e@øvŽÑíßz}qÝr½Ñ-—’Iv6§¡e²›ˆÖ~k¢[[ ¹·R@tË¥¼è–KÉ$;5Ð-)ÀçàYt;‡·\E·\ïHtË¥d’Ñ-I®2žE·2:ë)¾¢›¯OüUÑ-^«Œ>E·Œbæ2%ºåRRv–( º-Q¯}¯èÖV£˜[t+&uüƒD·x­äs=D·\ÚŠn¹”L²#º%ÉUƳèVFg=ÅW@tóõ‰¿*ºÅk•ѧè–QÌ\¦D·\JÊÎD·%êµïÝÚjs‹nŤŽè¯•|®§€è–K[Ñ-—’IvD·$¹ÊxÝÊ謧ø ˆn¾>ñWE·x­2úÝ2Š™Ë”è–KIÙY¢€è¶D½ö½¢[[bnÑ­˜ÔñÝâµ’ÏõÝri+ºåR2ÉŽè–$WÏ¢[õ_ÑÍ×'þªè¯UFŸ¢[F1s™Ýr));KÝ–¨×¾Wtk«QÌ-º“:þA¢[¼Vò¹ž¢[.mE·\J&ÙÝ’ä*ãYt+£³žâ+ ºùúÄ_ÝâµÊèStË(f.S¢[.%eg‰¢ÛõÚ÷Šnm5йE·bRÇ?Ht‹×J>×S@tË¥­è–KÉ$;¢[’\e<‹netÖS|D7_Ÿø«¢[¼V}ŠnÅÌeJtË¥¤ì,Q@t[¢^û^Ñ­­F1·èVLêø‰nñZÉçz ˆn¹´Ýr)™dGtK’«ŒgÑ­ŒÎzН€èæëUt‹×*£OÑ-£˜¹L‰n¹””% ˆnKÔkß+ºµÕ(æÝŠIô e‡$¹äy D·\ª*;çR2ÉΛo>æ€qÙ=Â&¯UŸóHzò,²+ ºå’TtË¥äîìüú7ïз»`+ÀRàØ ˆn¹Þ¯è–KÉÝÙyíµo¸Ý[–ÇV@tËõ~E·\JîËγÞ{啯ýà{ßÝW°Z)pxD·\¯XtË¥ä¾ìðÞ¡ÇŸ¶¯+´RàØ ˆn¹Þ¯è–KÉ]Ù¹øÆ7_5ºýä·«+°Ràà ˆn¹^°è–KÉÙ¡»ÍÐÆ'½o; ¹‚*¯€è–ë‹n¹”Ü‘f"¼þê× m|þî_·£À+¨RàØ ˆn¹Þ¯è–Kɽء£ ¢ñÞœ}2/l/W8¥ÀáÝr½bÑ-—’{±ó÷oÿ º}úÉÇt½ñöAM|ÛËëS8¯€è–ë‹n¹”Ü‹:Ú¬¯ ÆA:ËJšø¶—×§p^Ñ-×+Ýr)¹ ;TÙ j„Öên8h¢ÔÄ·]¼>òÝr½eÑ-—’»°Ãë†n——„ºýãÏ¿ƒIœ|ö¬9©?) ¶U@tË¥¿è–KÉ=Øi¦¹1`’ ÒÑÑìí·+t{ˆ…Â(ެ€è–ëíŠn¹”Ü…ÿÃ2‚j9ˆO 6Žg/^î" ¤8¶¢[®÷+ºåRr_vªÊAû’N¡•«*PUÞÜõ–¾¢Ûª µZãUå jURÀ¤@yªÊ›¢[ù '.T ª´0.º] Iªò¦èv¤¤u&q©*‰æŠ¦ˆQ ª¼)ºÅ¼2ù©JªrPUÊ(0R`[ªÊ›¢Û¶‰AOŸ¡@U9hFøu‹8ªUåMÑí¨ÉìÀñª*XgEM ¤*PUÞÝR_Ÿüo®@U9hs5)PUåMÑ­ž„¡D*PUŠ ³¼IsP ª¼)ºC’;X«ÊAÓVÑ‘K¨*oŠnK^¥îÝDªrÐ& è¡R NªÊ›¢[‰D¡r¨*9áÔ%)pn T•7E·sK~ˆoU9èz* R —UåMÑ-×k•b T•ƒŠÅZ’õ+PUÞÝêO0 aGªrP'lú*ÎYªò¦èvÎIq§q¯*íTC[ ¬¡@UySt[ãËæª T•ƒV©ŒK})PUÞÝö•xZ¨*éH)¨*oŠná½È±ªÊA{Mᔨ*oŠnÞ¸‘WªrPިɚصUåMÑm×ié<_U:ÏW XKAªÊ›¢Ûà;ÒÉš¨*Õ,”Â& +PUÞÝ ¿}=n¹Uå åÑ‘)pªÊ›¢ÛaÒÕùD¤ªt>²+¦R`Rªò¦è6ù¾ä¡6ªÊAµ‰£ðH ¨*oŠn¦=zžUå yQÐ]Rà T•7E·C¦±cGªªtl©;)¤@UyStKzwò\ƒUå Q¤@% T•7E·JR…‚¯@U9(>Øò)¯@UySt;|z;^-ýú7ïàÐ!¤@= üýÛ?{啯ýî_WC±#ºÕð†$È;ä R@ Ô©À§Ÿ|œ”£Wò,º­$¬Ì®ª€«ç÷ªB"¤@P ´Qþˆn«Â2.¤€›( ºm"»*¤€«* º­*¯ŒK) ¤À& ˆn›È®‡J) ¤Àª ˆn«Ê+ãR@ H)°‰¢Û&²ë¡R@ H)°ª¢ÛªòʸR@ l¢€è¶‰ìz¨R@ ¬ª€è¶ª¼2.¤€›( ºm"»*¤€«* º­*¯ŒK) ¤À& ˆn›È®‡J) ¤Àª ˆn«Ê+ãR@ H)°‰¢Û&²ë¡R@ H)°ª¢ÛªòʸR@ l¢€è¶‰ìz¨R@ ¬ª€è¶ª¼2.¤€›( ºm"»*¤€«* º­*¯ŒK) ¤À& ˆn›È®‡J) ¤Àª ˆn«Ê+ãR@ H)°‰¢Û&²ë¡R@ H)°ª¢ÛªòʸR@ l¢€è¶‰ìz¨R@ ¬ª€è¶ª¼2.¤€›( ºm"»*¤€«* º­*¯ŒK) ¤À& ˆn›È®‡J) ¤Àª ìšno¾ùø¯ÿú¯õ)¤€R ¯€X 2.¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ H) ¤€R@ ÌSàÿ¦l× endstream endobj 35 0 obj 102355 endobj 37 0 obj << /Length 38 0 R /Filter /FlateDecode >> stream x=ŽM Â0…÷žâ[êÂ4“Æ$nÝ# TQh±¦÷Ǥ‚ó÷3Ñ1¡ œüÞðé¹1Ò³òirª-e|½ð3ÿÊ—¡6«4pˆˆµK\y×؆Ö)g‰ÍY”FˆwÖ—~žŸãƒë{C|qŠå“î ÌŠ + endstream endobj 38 0 obj 120 endobj 36 0 obj << /Type /Page /Parent 3 0 R /Resources 39 0 R /Contents 37 0 R /MediaBox [0 0 612 792] >> endobj 39 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> >> endobj 41 0 obj << /Length 42 0 R /Filter /FlateDecode >> stream x­‘;OÃ0…÷üŠ3¶Cj;m)õ€D)l •†²t1Ž Äy9ÿž¼G H°ËÇöýιΰCZÍ+æaµö+¼BƒÜ ²hŽ( Yßš1ÚŒVìw«ê!õcヱºmV—-=xK ?yXÌ*þ “”9u µ‚±*R!J¡~ŸÂÿtîýÖWÏhŠö»–ˆ"[X¢Óos}'/Ïœ?¹ÇiMÄEıŒs ì"€œ)ÞÊHäßÜ ݹ”>–×½¶ø¿y9ß #öI™KÅù£2¢3ñ[þF¸lm½ ZARa>ˆIH÷ÉÄÂImiVdQhÔÜ Ê8­ü56·ì¿T¤L¢‡mÚýP£ªX endstream endobj 42 0 obj 277 endobj 40 0 obj << /Type /Page /Parent 3 0 R /Resources 43 0 R /Contents 41 0 R /MediaBox [0 0 612 792] >> endobj 43 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 46 0 obj << /Length 47 0 R /Filter /FlateDecode >> stream x­”Ër‚@E÷|Å]ƪðôͪ“UL•˜›q£Q.üË|R*JÂXP3@ŸÛݷ爎0ø=0- Çb†Ð4É_Hhö•fùUl^VCþ£a)ÔÇÔ…ifÁŒü©š} V߀ëCîi|îwœbxl³ â³$"”aÉo|và~)On¡ëÂȃ^V5D³'‰JA|ÊøXݽ¿ÙökÉ]u2"ërìJ H±´íeHÉút ñYÀå§+Méuùª#‰¿Î×¶g$%NxŠ)³í9K‰ñ{»B¨æXj©”BHºÕÓPMÖ%\Ï$iÉñ°KYWõN~Äõå2gS8{v`i´R¦·féDQ§ ¨W£¹ jÑ· ¥$ÜðºÇ‘ä@‘/¾½$Ì ÝfÓÐ\CÕÁ?f”•çnØb$ªoœÅ ïÇ}6)Êöš0¼=œ¤PYz„ÛƒùQzFVÎ5IX+æUŽ)a~f@Ó4£)¯_4oPaåê> endobj 48 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 51 0 obj << /Length 52 0 R /Filter /FlateDecode >> stream x­TËnÚ@Ýû+Î2HñcÁ‘"‘„ªQpRE¥ª&ãIpƒm°M¥|bwý¤Ží1c%¸õÂff ÷Ü{w‡9v°Ä§Ol †6bޝa^',É_YHXö+ƒXùS\NñGËÖX€+„dŬü['=vÏ‚ÀüÐ5Ä-Ü'œ\bÃãO~ÈÒ€'[Ê8îyèùásîOmì}0ò¢‡SˆDÒUˆZ8ÊúXÜ-gVâ®:"!ÖÍx¦åˆ@ǹ}Üohü*ÁÕ…@ך¢×Í«Ÿ+ø÷ó:Î Mé2ÚÇŒ;Δ§T6ñöº "t2T½T¨0·4]›idJ‘Mnf-Énã§üL÷öÁVô—·ys…å ßð4 [¡©+¬Y:Qòt æ•ÆhnƒZ!z¶¥•G¼î !’\óâ—dÀ”²u–†æ=ÔYQïÿÇŒŠyá [D¢úf9Ÿ=N³¤,E(Ûap<œÿ4HÑeéalÓWdt>Ò„·bŽóÊš’æ;†a°( 9K âÐ\ Â$Ú»}¨ûʆÒ$)zi^¾VB*ëà`B™/(M)~Ik {ˆØ•ôK `‘³˜Ó”C(¶v]¼…’A¡í߃¦ü™Ç˜}v1»›Lðeq;-ðiüpÚFw]K_‘8#ÿøC=/æI‚_4fk¯:mèÓ«¬!©Ï·ßßUåù_úÒÛ endstream endobj 52 0 obj 546 endobj 49 0 obj << /Type /Page /Parent 50 0 R /Resources 53 0 R /Contents 51 0 R /MediaBox [0 0 612 792] >> endobj 53 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 55 0 obj << /Length 56 0 R /Filter /FlateDecode >> stream x­YÛNãH}÷WÔcÆ‰íØ¹­„Ä„ A a aVhY!Ý/ñ_fÄ'ÎÛ~ÒVÛm»í¶‡\$ÀNðéSUçTuó·ð ~T ÆS Bƒy¤‚¥o)YôS}UI_ÙÍâjŒ¨h’åÂç5¨*}˜’þ”UCÍP`íÂàBïã]X?Cï’lòìx<Ó%Q`Z¾Ïv¼—Xÿ+-ÖÙº Œô¡ÅU†-ˆª^"Jâ@þ|xìÝßÍfË÷ñ„"ÂNˆm‡% £ÈÎfß|ËüžlÍð—7]Ú½¯<)áE¾³Ù¹›+? -2›]“Ød‹¨ßî"²:-×Â…b˜ñfû–äA > KêGo['&CÙNÜ×—.óü3¬^ɖľ×I˜t,ͼYœNÁ²óÂØ½ ZahŠ”#4Ôº‰ˆÒD@Æþ³#*¸6­ UÃîkh+EyôA1–‘Çj`°™$øwV·W˜OT)+ewI7‹ó·’­2¯,âñ;Ðp~7#ÒIqL8›bÅžý~ßò=X1F/vOPV$’à‡òtT–!+†ä¿îþøÖü«*gE2}A™ãK~Ö|HÕ8õ³ fÀ¨³˜1ÌØËýlþår¹ÀLºYnŸ/&/$„åÍ–÷WWðõîòúìîþ\<|êbuºRžK1 ~óË´íDü0Ckc†'t‘ƒ³!–Ÿ¿ýÓÅ“Çú¸0¸"óØj“¥BÀÚšQD"Iú}Koõ9ubÔ¤Þ=A‘”}v³JMCö•Q z~âÙ© @æªÑ,7RÆQJ‡›blØgˆ5eÌ#Jtp™oˆõJ9²6Vë­ÌMŸý¦ qMÀ²ñ^HT5ŽC£®©~E8JI=œ3rŸß\.¹Ü~0®5äVÕF#)J½‰­*löiN§¦MkT€§r½¸›9[ž_®ØþqSGšŠŒ0©w£Æ)WÖtE[%9Ëõb¹~Z?|å)}¤“g”­.õC)5€¤ƒ»¬ªÝX•6Ò„Ggržd-"‚ŸN¼)ôÓýkãa ÉìÿŽDþö´å‡hõŸnA2§,À÷éúíÁœˆæ\X&sæŽlb*š3VbaYsßñ8U}P‚M>‘© aê>‘`{)ü(kõû¹Rkø†Šèÿ<©kB[4f/"ŒÛaÞŸÉ«†–z·f›ú  íÕJÃIwÊ<Ź’¯ßƒN(VÑÚ(Îo:ò­ÌR†Z½…A:ò”áPì)U…Aú c'ëÈÇo¼‡gÃÌSª);RjS<”‘)J]iùÀº1#ðÈOô¯mâz@ÕÖ§ßž.Ïqºf#ueg¿e©Õ»A:}tg+WDÙ›+mªÍ„3Fx\éQ©Bøär–µ4pÆÐç~¯¯±ƒœˆMmO=5<ã?Õ…TÔô„EV•ÔÑ'i²®Ô»”[‹´OBâ9o ng|/ŠCk<¯¦®r=†©¹F8$[Üä ðÆ jSý¡¶¯EK\™éPââÉ=ä¨M"ǎʺÎùde"vŽt3iØ1ÌrÄ9%Û›±7ž-‰Þ£˜¸l :–嘳«Âš…c&: õw÷y¨µ}듊]°ö-r®Aq;¨Ú©hg àrÓC7| Eh÷´žÖXªhEç¾àÁ$IåŽ,Cµ?÷]׉Ó3 ¶‡Ø¯œ7TöCµa ÅÁo¾%¦G7&I°Ç™c{@uÑdRùQå¢xÜ¿Adc,ªü´ÔÖíÿt7I endstream endobj 56 0 obj 1360 endobj 54 0 obj << /Type /Page /Parent 50 0 R /Resources 57 0 R /Contents 55 0 R /MediaBox [0 0 612 792] >> endobj 57 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 59 0 obj << /Length 60 0 R /Filter /FlateDecode >> stream xeα‚0à§øG8ÚZ ]5º“Ô8“ŠFD8|)MÐÄëpiÿ^¾Pa€˜O!J«06¸ Gv` ÏK$À>ü"U†2ñq½•ó P‰ï°wZ/qèJ’Í­µHM®Èh¸ÙI“€„»as~]ëéÑßáÛš¹a"ÚÂ=“£‹[­BØï«Gÿ^®í ¤ÖQK~µ±áw;q°Õ9{ endstream endobj 60 0 obj 161 endobj 58 0 obj << /Type /Page /Parent 50 0 R /Resources 61 0 R /Contents 59 0 R /MediaBox [0 0 612 792] >> endobj 61 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 63 0 obj << /Length 64 0 R /Filter /FlateDecode >> stream xµVQo›0~çWÜ[R©l „LëÃÒNª´u«†¶‡iŠâ%l¤˜¨«ªþ÷ÙŒ`Y”Ô‘ sßÝwßý÷ð6ÿ ?ÀQø) & AÄä+X$VYÈ–£˜TO>ÿÐÆF”À»Æly7‘‡»„ Þ»Ÿ…ð'ô¯ "9LÖiNÓÜÚ$þ‚›°pE™•vÔÓ¿Aý& _i:ÓÅx\¢½QXÆ¡€ # ;£ÝˆŒþ–Q`yGy p±NÇ{” €G’¥<v„Ñ–Ó öbCI×]i7Gû,A?ZÆ)ç2'†”¢Êx[þ;) <=C¨ øÈÉlE§)I(¼½‚ÞäÓ]xsö.• &©3>dû:¤¶€ŒçÓÙ“@û~ÔÌ“#*êgÏÚ3ÄLÆxË«jA³¼\^ÿ[W¼"ÕQEù ðGPØmÜuBÆ—D†r„Ñh>¼íCeIÀ$$ZÆ)ãijbÑhÔÜ(}à7Ý Õ¿>ΞGº~·ªj7ôJÔ½š´s),hW˜€gÛYþ´) XLèÔ„ôO.}8y÷0±ÝlVNŒ*àÕšoZñ:}ÝzÃHÓk»P¦•';lì åCénU§§4WŒ5õRݵ ¨íÚÔKå4§OKÚI^9;*V^¥5aµ¶&-iBêåœe‰‡í*œ“œLÙz›EÅ.¦R}Í_|‘óãqíÈéMûM¾q&èlø8¬"Ͱ8ð3¦5ä"ËÙüŠ=ŸÏTçÒÏ”ó°É·ded1_/Ö¸òÈi NXŽpdp›¸p½æ‡Ëû¿¸šn endstream endobj 64 0 obj 643 endobj 62 0 obj << /Type /Page /Parent 50 0 R /Resources 65 0 R /Contents 63 0 R /MediaBox [0 0 612 792] >> endobj 65 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> /XObject << /Im4 66 0 R >> >> endobj 66 0 obj << /Length 67 0 R /Type /XObject /Subtype /Image /Width 570 /Height 358 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xí½a·ÛFz&Èc}åÛŸ}ùÍG§7­ÎNúÄ<ÓŽ:áŽ2š˜ëD^qïèŒÌÄvSöÆbÖrCI®˜«45íËX8¶i{DËæ‘dº%ØYÎn0I3iNO³sÂL˜Î™å8wŸ· A²€KŠ/@¾8÷\‚@á­·TÕÃ*Þçð7F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`F€`y888ø>oŒ#°é ¥ÏÛ)p:F –ܼyókÞF`Ó¨Õj±ìØ)F`^ÀVó&åtŒ#X¸¥'öÖ±ã\‡¹*0Û€·ôm¸Ë›]F®Ã›}¹tŒ€D€[:ׄ¤#Àu8éwýgæA€[ú<(qš8#Àu8Îw‡}cV…·ôU!ÉvŽ ®Ã––M©¶œaÞ˜o§šS$U1†hqÛ/Ûœº1çÜè–>'œ,Ùlv¶-åfwÓF¶5œ)ËÐJ`IâýæÔùÝì–>œ*ÙDZ‡©‹*†1òx=­´ÛnIQ‡§ hiãÚ•°aòÓ¿ekâÆŒÚšvª¤›ä‚²€dãèmÉ–~tœ‚ˆ¥ë°Ý,¥S…¦m™TºÑÙF%]ЭÃa%¦cÙÃN»©teÃÌÔÚ=Ë ÚhEô½¨­(ºO»QL§Š j°#kÐ ºt*­wØ—?2EâÒTbÑäó­¨a4ìZ!W¬SØlüdu,›Â2õä}ê+sZwhìAƒú¦Lk䳺€°b(WÞXÃ^§VÊK7DQ†Ý&r(šƒá O¿ÏÛet^Y½3¡+lï}XP0  ¤iËM`˜ÊÖÞp`jyìg=·¶ð[¹}Ä3¯¡DÞìcÆÑËÝv醋γß( —–Ûo{iÜY7t'ÛDêb£;v°#- ¥Õ¡_UcÐ'pR lLyö;ÍB:•­Môht»GÖ¸núï j‰ïŒè7SA ñ°I…+ H×Z¾²x¶äŽßs Q–Æ›ÎÖBŸ”¯ã‹?Ù4nâ©Oÿ%d9KoŠÍnãVø– L£m„ެœ y"Œa¯7žý¢‚—;£ÃYòõ®L«ü/ê†y8hd3¥Þ¡]Å…Â1êÉ—ŠÅB±TÌÁ;ŒÝ¬*¨»Ø˜µ#ЦÉq¡©Q7<¸t9.qF…äsÅ„ac--¾:Å3g¬v)]lzuâð“ܬ6œ­?òYU@·d“µÎ=zèÏTÞÁtíHn#*N®ŽFà%›, ›0øsù–l›Ï0ë@`ù:Lͧ‚.×e«vI°Õ¡Õm³òW<ºŸ­<&²›U­'zø”3¡$z$¯¢ï¦>SÌ›9€P÷•*K*r»/KΊ…|ÿÄÜ‘fè6eYÌ}°•ÊgE)»‘ æÕбÌnÂsoÞŒ8%‹¹Mwl¥ã›ª€n¢€ÏiËiÌ»ºg,1¤Èç~ÏO[²ã±Õ@L‘áf (À¶xf”+У<°ÌhHløeA ±é4m¶™Õ\¦Tɧ³uðD®Œgt[‰Ñ_7µ‡iÀœÀhÞ’•DZ´1ó+ó£ç_’­*ó°U¶\Ç k^ﺸP¢-e <È*·G˜Sþ˜p‹2Aâ `+ÿŸ¼-4þ8Èg98š, X>C£3YëðD«£åÆ£'AC.EÊIiÿte¿lð N°•ª€®ëÁŸK·ô`Ó|†X KÖaÑŠ¾ ]ŠÜ+ÒšpF =¦³¥‹î‹A+pV>ïöžÈ‹ô¹–p¬œéŽ”ï )g‘€›Y*×ìKB°šÞÂô{5ÌJµM³Õáh`øVYäÄ Qû¬,`»ZKJÒ…R 3]eÏ oµ@®æÓ ußš“bM®²« f Pµe»©ÑƒlÙØÅÇV£~µËæJ†r€Y:LãeûuÛ¦5D×_¢»8¬Zµ‹ÇMøy€;rùáH®™ÉÕF‡"`<ŸÂZ ±a†-•“}ˆ#z–ÑÜÁÚéåàBŒU«òª.fÅùUzˆYIoëM£&–É/Ss¹ä³†±æ¨Û‚ç•L±åæã]éÛ¡ûD.¨4õW¢1 mrµC­ûUÏH3S@š§ÔC¨Ò,øQj‘9±Mfj× Ýº[²Çe*T@#!K¶ôË|ŠX+ªÃ‰gÆô9ñ³5p5¸÷ŒY”“&²Äì™âg/V<ˆ5 ³€`9ñDfH¡ÌoöJyd†­œÃâÅ&·£p.¦uæ“>»‰1²ewŠ™À­I›ÝÄóô™3xó'àÍ)EgŠ#jËèžé,uþÁi€ÉÖ©Ñc¦rÅR…6ZáYnÿ©^¡ šNO.çK—ÅŠ‹É‹Å·Ñ &¸¿ –Ž`À‚ë0æKC/ÒÚÚtg©¢ûó&[ÆÃÇ*Oi]èá¨'V‰¤Šu£Û® *¢õxø]Q)–Ê%z:•-â—- 8 RX[j™ Œ¤h€iü±ãs®P®`>‘~Óø‡-žz5…§é\©Ti;?„(‰|A»Tk´be_A§)’Mù¬. ±U*ïù\Æ8Q2u¿UCët¾Ls´ôCËîbJ!•.h­VC¬¥l ýÅàoE-Ý5ÇŸŒÀÚˆE¶åºhšè˜Ýl±$¾îõi³)áȈ֧‰-½p7~Tv6–s•Ěƙ”=¥)+‹9“v±–õÛ‹ÓêÖзÓt¦ +üÓja54tøUükwÍNj•jÃTû:ê×É‚N ѱ ;5\Vkãت,lWëôu¼!SpE&›/–+¥rWb}ƒ ˜ªzÔÅ®†wÀVšàéa¹B„–ŸV*Uz{dA‰µŠ¶’>ûJøvƒðY”P«”)£±o×4«ùl&“É•E)œS³>« 8ê4€¨pÊEµÞ¦Á¡8\¬5½#~cÚ½v9ŸC~ùbÕò€2[ùïïo,±`+üì†éû!ëÃï@r¥ ÷c| ÚõMdi£×,d7Ål ÜÒ7æVnmA6º‹Îj ÿ‡o=üÖÞx.ø–!°Ñ-}Ëîå¶w³ëð¶©Blk-ærÀf·ô£ËÏ)’@¤u8HPCŸSPc!Œ¥ååCðÊÂÚ*²]¹ü„"G=´Ð{fá™, BnˆÏÆ H[zœ ʾl,KÖa/E²C<·½ùÁœˆ×g™ºxÿ‡Óù<íºQél1Áñª¬æ R§¨"ÂctüB®/»¼‰«z:l¹–§­ˆe'¶*y‚OüsÇjG2`Žˆ÷g›B¼xK)Ò¥:¢Ù9Qáo³åh£Èì(Üœ#qбÏ—·ÒËY>Ÿ½ ò‰×¦=çïqF`É–碱o[‚ÀÒu8™Š!j-’.½2œ× ‘Ñïè"¶€Ô%A]è,…bÙF–IÚ' 4_ÊQWÇa¡7qhSD†|-0î‘ÝCС¼+wb ‰Fo@Ê,^`¢Q—Bñ”[¯ÊYâê[½‹Ãà»'eVèEd™µ ¹.B^L‹SôI®¥dðªˆXë„ir}&åŒáB+Œù7>O–néñ,{µE,_‡E v„=sc°'V1DÍhº‘&úˆQ˜vU1žöÚ-ãHtø$NAÑFÝî8ê«V}ØB<‹t3,ü€”·@ˆ„B¶Œ8r,HˆV¤¡°Q(‹EDFDˆZ…þ…*åÄD@¸<7¾½¬ÙtËÆTø6 II* ])ßC†Ã¢’̘‘G´F[%¶oX¾¥'¶èìø† °|l…ù-—­6E1D°Õ8^zÁÜŠˆûî’Yàý§‰µ\Eoé`¥q%9±–Éш­ª %Çq•6¢¾bË}„㓃Ò„éfKÄú͓ڑ0ëW²pÝ¡nóÏG+!"Ÿ'þ+ÎOÅ‚\8éËmÄMϵDœ!ã’×Û-„4?:`»›3Æå[z<ËÅ^mK×á¤*†`NlF‹d!'Äkmwûf»F‚Æ<)DÐæ¨‡TZ6Ÿ†Ì-­ 0å("ï ZÉ@xÐOaVÀwážOç´z9W¨B %'õ0ë—Ãs«f,e\V!øîŸ  z"¶ÝBl±ã¬ÖDLõL‚•LÖe+’êÀH3—eh¯–ÿ‘z¸€1]¨‹±K€ù`‹~ û$Y›Ê@qCl£ÆY’)8ÂÀ#’í øèéQÛÈûÀu¤!5¡9)Náêz¤RBmx¶’¡f…Ï ئaöZ oÉbÒþŸ nÝúÞ·žýFBÿþò/š ¨ÙÕèX’­\Ç0R±¨G¥Ï‰gʕճêÇ£¤E"WÀµüÜRýi·«X±€…År¹Œ˜ß ’ÿë·…tFº¨5;´¤”RhMÆ÷öl„„1®ú2uÁºỈŸÕð ÐÙzÂ`¾‚;¹nC­Q[(]f‹µn¿#ÔS•&ñ›BœbØ‚XMˆgdd!¦[_õ[Âç •¤¢UHÑbñ@üy¼üã?Úo¾ù›ÝîJÖßÝ»M0ì~ôÃãEs +b«åJ³~ÅáoˆI·.yA-b\Z»MªBHÇÐt£¶¢£ÐÙ€ºF§)Nc”6AëžM›Wj g‰D¿ ½‹J c:G€WO‹Ô@9Œür¥r¹X¬öF#£.}¨w{ªΨµˆ­â–IÊ Z ‚…¢t=¾r}vCð¬Ú\ÇèyÏ;qE -}Apz½.³Õ‚˜mròxÔáõ+†Ð= Ñ"Á @’ 1Ùä ÁeÛPâÑÒ—Ùj1¼6=uëð¦ß.#°z’ØÒ™­V_’l1‰u8Éx³ïŒÀñ Ä–Îlu4DÐ y2KQ/á)v½æj€c¶€a–ã]uÙ»…X¾¥/”ÝJ3[­Æ1²|¶ t¶Y™ALÓ9ñy†Ä29 †ÚmPÔž"±˜Õk€Ò…ª9t[U°V¶Ú‘`ˆ —« l«k 2C^—‚L‡£F‘¸-Sn{˜w4긋ˆÎÐïêeÚ×{#¡3˜oõÀjôj¯VÈ]‰(ïBoÇî!ž^ZŒhõtÌ)viHAÎóµŽe; ŠwŽ©FHE‘°T¡i“GºÑÙF%]Àñ>ÅH/Ôú¬nÓ–ùf_Ž çÃþ¸Ê‰Å¦ëžÒCŒž ô4„¨†Pðp‡¬2o¹ÓZƒn½tó]„¦õP„f’B] YvÝáÏ@`ù–¾~˜­Öyœs\A¶{ÍFC¡~J4q,Щ§ÜLä*‰>_M,}MËèFBhcFJCˆ:¥*5ˆ ¦½á4J Šè¢JÑѳՇ´ "Sj¶[-Íh·Ûfˆ•rÔš®<#•AHX*Sn hfoˆxçŽÞ.&+ TWE x‰ ç…jë=!–‚´Ûwl5¿"÷QZ½]GΆ3ºR»AåµÛ®«né­6×gCíú (b0†PmÙÍ?7´ôµÁlµvÈcá êð IƒŸt6—ÏK¥ZÓYùà—üïO±xÇa+ºÜœ|fu("¥çê@$(ì*mÄqc¶€­òõÿz4Ö)òyü_DðsyÉôr#…ç´AÛÝñáÐê6‹Y*Ü|leŽÙJ xÙå‘ÉA1_(DäöJÇ᛹Ű!5ðˆ2¯;Õ•n«D‘“ [Õ=¶Â´¬Ö×"% (•%-£@ÉVêª-ÓQÞ6´ôµƒÁlµvÈcáòuØ&iZ)Àd·0GçhBÙÂ(¼mAZ}<¶¢gK¹J§×ï4q¡t·ºu Š©l±REìW Ï^¨ÇºH•šÔ‹‡;rîÎnÐ|Wª 5»=³FϪHEפç:Ù¦˜Ü5$(·z1QNÊë‡7ˆçÜèµ–Y¯ébV­ƒ°¶E²$à5 ¹Â|Uxj·*p#ï’íÜbX£® aò³Ox9œr¨pŽØƒ.Í£>½Ó5[ºnàB1˜)µ{˜áÖ%óŠ¡_¦TÇÌjûˆY†S¼mË·ôõcÁlµ~Ìãœã êð¨GÂÞ&ØJ.½Â6FBØrr]ŸÕÎBï°è\P¬ŠIÁ¡Q!év,Àà Rî¥fÏãa4m&Ǩð§â¸t¶'ÚÖ«w$+YMŸÅš15NóÝ[®XȺè’Ž¡“Ø-Ç* §‹Æð0LÀËÕÚnèË$æòÚÜ)U9£¤7àœ>Wi5pmrèwè_eA¬ÑO°•³ºŒßp—ú/dYbÂÿ7´ôµãÀlµvÈcáªê°\A¶‡ *4zÑL…NÓۦʤ#¸ÜF~H©­¹ì`9÷ô’yôô ‹ xÍ“÷¨ß’Ë+Ä•VSsV“ô\–U¹áÙS¥¤rÈeðòrñÜŠàœ·€–Ýü™lVÕÒ׉³Õ:ÑŽ^k­Ã¶)T£Ê¾Õèc„:µrÝ7<ŸØÐ=Gô šW‚­R†J«cX¹ôfc‰‡BÏqicl`#XkK_bÌV+rC̬·z¦a°hTôug(€vÖ|DŸçÖÛÒWƒ³ÕjpÜ+I¬Ã›‚=—ƒXIléÌVë«IÈ)‰u8 ¸²Œ@¼HbKg¶ŠW:no’X‡3ΟHIléÌVÉ«gQzœÄ:<7§bŒ6…Í0)ÀoŒÀ$±¥3[-pƒ· iëðü·eÃCl“^—–[N£7¯ycæD ‰-ÙjΛ»%É"­ÃAâò¸|=hµ8KËá¯|•#^e*}[Hä¨\&ÎÛØÜ׬¼AZ$H0¤×гþ·ÓB{yg›ˆ´¥G,³UDÀ&Ôì’uØ“ ™ÔP‰SÐk° 5]=É ¼€#Í´»ÎðŠžqg8¸UÓ‹qáO\ {ó+†ª-Ã9SwcY¤ó"X†/âp^ÿðG¸¡—žDd‰© Ÿ0¾?ŽÀ‘Éëšâ Ò"v›åB>—/Õêè!,«;Á:<ÜÆò" ­¦ìöÒ,ÙÒ—ÎÿQ 0[= j›{ÍÒu8HPC-N!t=2µvϲ†Fá+R2¬Pµ( ‘›$B„¼”8 àxiR÷L†xsÖ'1N¼bH€å.½¼œ×Ìþ°ßÑ)8TZ màöÏ/b°IŸMxïÄeWúLv €7ְש•òBåD­E"E“ÓùrUƒ„ ¶¬ˆÙX-/²¹õ˜KvK·ô£2ˆà<³U &Øäòu˜â«Ï j*e/¨Ï‰§ó©Z޶ËT"ˆ£î!ލ‹ó+†¨,Û]Š…+ã–ã|aßÓ"æžH ѬvË!‘&ÿsùLi­NeÌV^,b³Z$¤À’«ËùB!¤2µÍ&&K³ò"Ò>ÿßJ–oéë‡Ùjý˜Ç9Çåë°`+Lñ¹òORPý°B}C²•·<ÀnVµö€:`ê´]U Sˤ}Ú‹˜F˸³^Ib«TYj‰aÐÑ4œW1Ä14i¹ß[92"[¹AÑ¡+Òª•Šå†9ßb¼¹|&'F&Ø a{}[€ ©Uz(V‹ô°pY@b2G¬7)/âË„w· å[úúc¶Z?æqÎqé:$¨™U߀4ž ¥«-J·-‚Ù•ÛÔÿËI~ MÌsy‚Â&Û7uïD²× Ù^0‡eu!•¡‘ÝbŠ!H¯²,uxKínßl×(r¼£~‚äs‹€HgØJ鳡×*E’•B)µ¦)†Nj-¡™’ª4:ƒ~G<ê’ú)êÄò"Ò9þ¿,ÝÒ4f«c=ÆY.Y‡Ã5Tâà%ÿ* ‚& '—=äZbÀJ!FÚ|c»#‰g7tÊÓ—Ÿ[1÷Ce‡­NYè*‚¨òسÕa§>—ˆ{«i¤Ùñ–ùøÜ®bÑX1] ¡”bÙS9 Ð"T®§âK÷T‰åE\ÿøsÛX²¥ \ÌVÇ{l3]Q^LPƒÖ™[–×—ñ(Š&İ||bZ çFö ïiÈO‰eÛÓ‰Ãd6&®¥/A–å ø.ÖÝgg.V  ÕU*—ˆQ³¤ÖUŠ©>å3ͦ”*Jáu¦8Vîy‘@+|bsXQK_+@ÌVk…;ö™Å¢Û¦.¹¾“ ÉÅuOöiòì2ßB,;cOSxîlÀVZ¹RÑ € äJ¹r„ãP1.”Óœ;wvœ˜X´ô9}u“1[¹Hð'!:lw;J"£A»+‰ùÂß²ËXhF‡ÅMV 9›;6âÑÒ+>³Õbxmzê$ÖáM¿'\>F`õ$±¥3[­¾$Ùbëp’ñfßãA ‰-ÙêxêJ\sMbŽ+–ì#_’ØÒ™­â[ŸŽÃ³$Öá¹q.äšï‰ÿ¾õðsÛŠEBV ‰ÅmH¤IléÌV‰¬j‘9Ä:¡-=¦Åc¶Šé9&·Âê°ÕFP ’g ÝDè¿Y¡d‘Óº6´2X*ÓâeXØÔY==“.vݘµ5WØV×hsy½k Ëù©ºÓûOZ!'5Qßp]­EŠ!pñáä#ÖÒãZ:f«¸Þ™ãñ+¬&ƒÝ©T‹€Œ&ã™v!ElýR%e¼ÐF–– F×—‰X#¡Q)†Ðu õ Ÿ9Å®ß/bÅF|hCkéq-"³U\ïÌñø¥¬ÃF]£àA•"¦ÈòeìTjí~€" alåªrôêvr8FZ:U‘1ÈœE@TŠ!tB}#ÀiçðT¤ôIŸ…¾•G¦¬%Ÿ;Ê–s§™­b~ƒÖìž²SlpDd-ŠÐ¬Ø+âa¤äÕ›˜¯› œ DððL±Þ·,£Šñ–`+«[×Êxh•-JéŒj»O‘óÌd¡"‚ýAR‰eL&BjJ¥ ¾¡öGY1$>±‰([zÌ Êló´f÷Bëð<3ðwb•8EŠ€ ¾«o•EN§U6ñ(ëÈ}”tZÛp8DÜrbÆLž¢– ¥¦¸ÜRˆ€,ª¾¡”CÔ¸ðÑME ´¥Ç´ÐÌV1½1ÇäVXžo•…t|VD—o<ùÅA¼ô“kÕG¤ÜQ#^?¿ú†]V QãÂG7°–×â2[ÅõÎ_¡uxhÝY¢‰ÈÑN­\7Z€š÷¢ê¬ 'ŸÜ(B[zLKÊlÓsLn%±¯ *V Y’l'þ$±¥3[Å¿^­ÓÃ$ÖáuâÃy1›@[:³ÕfÔ½U•"‰uxUeg;ŒÀö Ä–Îlµ=õsž’&±ÏS.NÃ0~’ØÒ™­üw÷“X‡ù®1ŒÀ¢$±¥3[-z—7;}ëðfß.#IléÌVQÔ„äÚLbN.Úì9#p\$±¥3[Wm‰g¾I¬ÃñD’½bâŒ@[:³UœkÔú}Kb^?Jœ##t’ØÒ™­’^ëVëëðj`kŒÀ6 Ä–Îlµ 5sþ2&±Ï_:NÉ0$¶tf+®½~Êåïð#Àl<¯½öš¿á'bŸÙ*·imNjÚ›Ïø`6ï}ï{këUV•³ÕªÜ ;Úµï~ñåçüÇ0›@­VK\—Ål•¸[©ÃÌV›ÝGq鉳U¤)_ÌVÜ›1Û€³ÕºSÎ"R˜­¶¡§â22ÌV‘v¤l| 0[q?ÆlÌVkèN9‹H`¶Ú†žŠËÈ0[EÚ‘²ñ5 ÀlÅý#° 0[­¡;å,"E€Ù*®=ÕýæGºþÎÛï}ôÁÕ+Ÿ|Lgõwôæ½ûq-¿#˜­"íHÙø`¶ŠaW÷+§O¤ÆÛ‰§/|àùùࣽ³;ϦR§_¸ò‰d´öþI:ñtížÓO~ró‚L¹{pǵpçÒÛ½yÛ=òù݃‹2ÙÔÿ“/Ö¾xT›žqÞ‰ÌVkèN9‹H`¶ŠCOâ÷áÁû¯y”qâĘ•.¿Ó¢dŸîíŒO?6æ´Ó¯|†³³<å²Õ»gÝçÇÜä°•ïØÊ!5϶Üyâ…ýG¶é/ï;ÌV‘v¤l| 0[{72é@ëòiI_> ±Ïg]s%/ ±Ò$[鯞’§ðßÇMÁlõä9ýÓÛÍ?ðþî¶ï?²ÍÉrÅhNl;c¶ZCwÊYDгU¼ú®{û;‚`N¿ü¶çXóúó;;OíÚ¯ˆpg‡& [rà\luâÔKW^{]ü]~õ•ׯ½Eƒ.J£óÛŒ°ÌVÌVÑw§œC¤0[ŬSýà9¹ŠïDþ½‡rTå­»È7¿tH¤ž<÷¡ÛýÞ=(I9ùâ[“ÌòÍ[davM…Ã_»b‘¡°ã¬ ܹ¨@c‚­°©0åú̧ÖÏFÚ‘²ñ5 Àlµþ~#<Çæõ¼34:ñôù/œq_­ÚÙÝÇ…Íëçܳ§^ºzõõÝÄ©S·Úã 3Wå¢A­þðõ³§Nž>uúôÓb’0ubçéÓ§OÜ9uùÖwûã"ÒÐßÉ“§ÞÀ²y[-d3¼˜|vÍ0[­¡;å,"E€ÙjÍÆÙݯ½øM‡’ÜÂZð@÷÷wÇëÒÝóƒqèì½=ÉD©'ŸwÞþòÎy1X{îÆÈ÷’ñçs7nß½ùüø»oïìÛlsŽ’Êñÿu ÀliGÊÆ×€³U<;ÕÏ>ª½þòÅ]¬ÐÛ-íÝò?¥¢žíîû{—^ÈŸ9óì™3ùÝ—¯~ˆQ•䲇·n\Û»~íÖÇÞ,ßý÷nîí]ßûðžØ¹±·?ù‡Sú§÷ÜÓ÷¯_›=õ!bb<ªÍx»µ^1[­¡;å,"E€Ùjk»/.øV!ÀliGÊÆ×€³ÕVuY\Ø­E€Ùj Ý)g)ÌV[Û}qÁ· f«H;R6¾˜­¶ªËâÂn-ÌVkèN9‹H`¶ÚÚî‹ ¾U0[EÚ‘²ñ5 ÀlµU]vk`¶ZCwÊYDгÕÖv_\ð­B€Ù*ÒŽ”¯f«­ê²¸°[‹³ÕºSÎ"R˜­¶¶ûâ‚oÌV‘v¤l| l'[ýàÁ½«o–ñ«ú+.ì6#Àlµ†î”³ˆÔáïoßví·ÞüÖ³ßÀÿí+:—xK¸yóf¤=IÆ{½.Úé~ôÃ(Œ³MF ÈV€ÿ‰ð–d¶f«í¼ï\j?ÌV~4xŸˆ'ÌVñ¼/ìÕ:`¶Z'Úœ#ðh0[=n|Õ&!ÀlµIw“˲©0[mêårͳÕüXqJFà¸`¶:.ä9ßø ÀlŸ{Áž0A0[!ÃÇ·f«í¹×\Òä"Àl•Ü{Çž¯ f«U!Évè`¶Š[¶œ˜­’r§ØÏmF€Ùj›ï>—]"ÀlÅ5ˆ?ÌVñ¿GìaÔ0[E0Ûg–G€Ùjy ÙBÒ`¶Júdÿ·f«m¸Ë\Æp˜­Âñ᳌@`¶ŠÃ]`Žf«ãÅŸsgæA€Ùj”8Íf#ÀlµÙ÷—K·0[mÆ}äR,ƒ³Õ2èñµŒÀz`¶ZΜKœ`¶ŠóÝa߉³×F€ÙŠë#˜­âØÃ¨`¶Ša¶Ï,³Õò²…¤#Àl•ô;ÈþoÌVÛp—¹Œá0[…ãÃg8 Àl‡»À>/ÌVÇ‹?çÎ̃³Õ<(qšÍF€Ùj³ï/—n3`¶ÚŒûÈ¥Xf«eÐãkõ Àlµœ9—8#Àlç»Ã¾1f+® Œ³×F þ0[Åÿ±‡Q#Àl5ÂlŸXf«å1d IG€Ù*éwýߘ­¶á.sÃ`¶ LJÏ2q@€Ù*w}8^˜­ŽÎ˜f«yPâ4›³Õfß_.Ýf Àlµ÷‘K± ÌVË Ç×2ëA€Ùj=8s.qF€Ù*Îw‡}c$ÌV\f+®Œ@ü`¶Šÿ=b£F€Ù*j„Ù>#°<ÌVËcÈ’޳UÒï û¿ 0[mÃ]æ2†#ÀlŽŸe″Uîûp¼0[/þœ;#0ÌVó Äi6f«Í¾¿\ºÍ@€Ùj3î#—b˜­–A¯eÖƒ³Õzpæ\⌳UœïûÆH˜­¸&0ÌV\ø#Àlÿ{ÄF³UÔ³}F`y˜­–Ç-$f«¤ßAö`¶Ú†»Ìe G€Ù*>ËÄf«8Üöáx`¶:^ü9wF`˜­æA‰Ól6ÌV›}¹t›³ÕfÜG.Å20[-ƒ_ˬf«õà̹Äf«8ßö0[qM`˜­¸0ñG€Ù*þ÷ˆ=Œf«¨fûŒÀò0[-![H:ÌVI¿ƒìÿ6 Àlµ w™Ë޳U8>|–ˆÌVq¸ ìÃñ"Àlu¼øsîŒÀ<0[̓§Ùl˜­6ûþré6f«Í¸\Še`¶Z=¾–ˆËúÛ7¾;ü²øó?ÿÓo=û Óì`ÿ«¯¬ë×ó¯ÿú¯¢Ëš-31D€Ù*†7…]b€Àÿïÿø¿þËÿåÖÔ°ÿÓŸþ°ÕŸüÉÿƒýÿpûÏ|;ûw÷·Œ#°U0[mÕíæÂ& ½ë¿ù+¿ræŸþéŸülõo/®T~-Yaoå`¶ZC¶ÀD„ÀŸüéÿ‹!Õ›­~ò“Ž<4îG”#›eb‹³Ulo ;Æþ÷_ü­ßþ ­þÝ÷÷ÎýëŸF #°m0[mÛçò& ýïÿbþô~ôC9Èúå_Î}¯v-YE`o• ÀlµÙ#ÿõ¿žýùÿéààwÁVoTñ¿÷Ÿÿ,¢¼Ø,#g˜­â|wØ7F”/ï>Wøà)üÿ7Ï1&ŒÀv"Àlµ÷K îÝ¿ª’|øïä9»Ê¬f«‚ɦ(/^­ø5«(àe›IA€Ù*)wŠýÜfðâØŠ_³Úæ:Àeg¶â:ÀÄùâ¿fÿ;ÅF‡³UtزeF`…üÚ¯ÿ*¿fµB<ÙTâ`¶JÜ-c‡·,eß΂s©‰³ׄù¨×ëoñÆló7NÌV»‘foÞ¼ù5oŒÀö!P«Q$|ÞŽf«ãÅ?Y¹ƒ­’å0{ˬ®ù+qI#ÌVK¸U—s›ÝªÛÍ…õàšïAqŒ;ÌVÇ~â²æ6›¸[Ư®ù+qI#ÌVK¸U—s›ÝªÛÍ…õàšïAqŒ;ÌVÇ~â²æ6›¸[Ư®ù+qI#ÌVK¸U—s›ÝªÛÍ…õàšïAqŒ;ÌVÇ~â²æ6›¸[Ư®ù+qI#ÌVK¸U—GÚfG¶5ZJƒýÜ÷òòPÚ9“+7$™úí‚{0•J´Æptd™Êhw„©t½;»ôr,ö\tì^#ë¸Êò”sF³lÃ;(NfE9pÕ(çÜä©‚Ö$ŸÝÄÍ´»c¸ e×!þ\;Ë×üµ»¼2[màM¬H«h³ö[·‘N¥us0è,° +éT¾Ö±ìa§ÝTº¨£“·z ðVºPEÂn«ŠãÙjçð°_j} ]FV·]/dóÍ>¬(- ,†-Inåöðhl†-"ËTªâ&6*™TVëYöÀlK¦K&Øy[ƒF1*6C”ƒaëh ±L­Ý³¬¡Ñ¨ñU¯:. ¶´åU Î…, óüïxXEÍ?Ï7)Wf«Mº›Q—eemÖ62©Œ1èXZ:•)74³7¬çS™ŠAe¡dé¶›¬£á[e(Æ)…j£Õj6šÍV«Õnwªp.ñ[v ˜FÛ˜kdeV³à¦f-—ÊÕ]}>eB¤“®ýˆ­Sɤ+ Po³Àµ“GœSv§â]HW•©€T¢y-{YðÎ1 °²š ¾oN–ÌV›s/£/ÉÊÚ¬`«¶×écÕm³rDã5[aŒ#ØŠ&Ú2¹b¡/ 9ºJŽ_€Ë•a£Z.•k]_Žhõ1*4‡Vƒ¨–ŠÙ½Zi<¹‡i@o„Ft“¯»t “’­ÕRv³ªµDzØ*åМ©­Úâüü–)9oÇ…ÀÊjþq`#òe¶ÚˆÛ¸¦B¬¬ÍÎrŠeÖkz§?´<5*¶„:†|Ü“«tzýNS+åëæáá€æ óÕ>‘„ݪ`æ-oÊQ Q›Ÿ š¨‡¶’;¹GU›ÕA.ÙŽ°\˦rµ®Hez½it1Ø(¥SÙš¤'3%1k˜¯š½n³^ÇO̦«-%iiô¸LN? Ë©J³k M²ASšØ°,Òó¿ãA`e5ÿxÜß\™­6äF®¥+k³³œ2h-¤‹†½XmÌÊ•ŠÎ¸¦Xu'‡Ò8uN7}C¦YË@fÔ¯rÙ\Éܨù”3êÁhÈ(ƒj„#êohUiõ…“…زåzMrV*U%O&VY &ž[I¶¢ h˵O²â8ŸŠ•ÕühÝÜLë_}å4p?[ýýßµ™¥åR­•µY%§`¤UéC±ìB:Œ±U¦"2³%˜Nì^¢[Í^¬:Òo‚žhUÍÝ ²Xý—×Ä”OŸcK.üŽSbËWž[i( ¬O•h!Ë*ÇùX䬬æGîé¦epãÆwÿí¥‚,ÕÕ7Ë…ÿí[¶ýøúÚÿùo߸úM+-—gu¬ ÍŽLZN[zz¾nÊO[¦”£›©sª¯ó[V]cýv½¢iZƒ'b+­\©V5­î°UÀEó¶M±„¾â=ðšï2NVPóãR”„ùñиÿ­g¿ñ“ŸÐ %o¾Y.—w±ó7ó×ÏþüÏÜù䃄†Ý]#«h³£¾Ù1°uŽ\ò0ꙆaÒÚôù¶ù-Ïgo•©ì. ½@YV™7ÛZUÔü彨F £ÑèÜ¿þ¹÷ý=Þc«wßûý_øÅÿùþá¿m#"\æùà6;NœjÓàšŒwô{µk¿ü˹¯¿þ[](þËïj¯£Kœuüà6ÿ{ÄF×ü(PÓfï?ÿ&¿ü£H¶ú³?û|ýÿñ‹9/çdÛ‰·Ùí¼ï\j®ùÇ[þÍÅsÿ×/K¶úÝý«Xkñõ×_¯Kœ{̈m›íø—˜ËEâ?bñÅÒõ»gõÍ6mFbÑâúIRޱ­ùIq _?øðߟùö?»üêÅ_é…³¿ô³õßß_Â_ºÄ·ÍÊ%ãÖä6´æ‹‚¾÷Î+¤xÙaôºÿU5/ïÌ ßš?ãêFø»¿ûÛ3ßÎþü™ŸÁ¦ÿâ/ä;YV.ÔjXU›o)¨d敦‘M|ƒ—™  2»6fG•E%Ë3ò"ÒѬa¥ ¼*…m&ñz…K<£Ã 4 ØD)JX O’ñ6œZUÍ߬"*ã•ʯ§ð÷k¿þ«eÁf7 U´Ù‰PŽ ºÚA»(CŸÓ¯þ,b·Éø)(†xïÕZ]ÝKž-ha«á‡†ßrµ-• ë¾p™|¹@N*]‚.”CFýŠÎ"“×…{*á'À…3tAd@ÿTäÜÂ%þáÀ±ž,DÛ[ÁÊ··($½|É_…†ª€TG]Ëçr…²VÎ"ì¢ i¨Jì¢á–Å“DÙ¤=oYVQóçÍ‹Ó)/^­ø5+%>|p åÛl€ †Éi]"ºÑT‘Eì£lKìÖý/å4¢ø l«k4й¼^}€„ˆŽ$"vë")ßI]|«N {†VÈ)¡Ôõ(Mézà¸e &mÞXÃ^§VÊ‹Äjá â`R¨Š‘‰GCs —Œ`d9(§‰Mh÷t   b¶zÈ¢Øu­Ï¢!pVÐ A“T¡RÕŠ´“Ñd(xìÏ&VK¢LU‰-ùº|Íß ¢+&^¼úWç~Ž_³Šá ³¼t›U jŒú ôœ|în]Œc(¬Ýk6z%´I4f]‘ ±ÏsurÖ½Hùiµq™>=Ém ªR³Ýj@t¤ Í‘¶Ù§aŠR×Ç)‚ºO(„²Cµp‰ÝFÀ('´%oó —àOXĹ^„«r1©Ü,fÑ*à±¥ ¨k á’ Ä”íŒ$Šã˶},]ó· °HÊ‹¯ø5«HÝD£K·ÙA ¶4I$ÍåóÅR©Öôèi=¬Š9ó isÁVu­0·¨5iVML늅|ÿÄ—EX"¶J•% ]Gûcd:QþÆ9Ø]¦.!"˹RÂã+ Ý5·p ]岉kØÊåô^ÄSœ3³hôPŠŽÊa¯–“l„ÌÏH¢¸ÎlÙçÒ5ËðŠ¦¸xñŠ_³ŠÚ ´º|›3T³‚ê™@Û„~GNêw´´éËT«[×Ê4Ð*"ކ]xå=ΚÅ\ÌfJífÔ:4Å(:y“â§g!ß F ´S%½é—¦eu¡¦•J‹†^«Ð¼YVæWkJš .±m(#Ã=x®wºfK× wÐ8·p‰,Æ[a&Ü)Öû–eÁ¶ @# €#R0Nš8i×àdºÔ‰dX!‰2 ñ6Y¾æ‡ T¯×ßâm>¾ÿýïÏ—pÛS„T¹m8µŠ6;±Ê]§Ô ük!ÐÏ‹qè7^Û€¤’­†F¥H1b34Þ* ¥f/„­0 ñY¦î^PÔ}¦‹5ƒÆiÎ*ˆ èhË5EÂvµ\ yÈtò+–eb¤W —të"jo®ÒjˆˆîÈÐYÉ€Gdó —ȺDƒ»±0±³~#ëjV–tzІÒ ¡* Ò“‚KSÙœ0•«ÂŽ2±›ã´$Šôn»þ¯¢æ"ãxו7F`…ÔjµÀ ·'VÕfi±ô¤ †ÄÏïKMMðÉÅç#¬Ð¯5Ñ„tÍ <,;«ÏýÖÉ´_ècl 'üH3)µvI° PäAÜ•cÛGïõ[5#Í4¦óe=dê^ô&Wî£æœkÎû M<%‰â]´%;«ªùJ¸"5®Ì‘n<\©âƒ@§V^ç›­˜…+”Æë磮êC/—¡^B•JEÓ;~ŽÍ}ÍhÌ:°ñG"­ù‘ßø[ÃT"À•ŠPV >¸ñDZó#5¾ñ·† ¨D€+# ¬|p㈴æGj|ão P‰W*F@Y1øàÆ#iÍÔøÆß. ®TŒ€²bðÁG Òš©ñ¿5\@%\©eÅàƒ@¤5?Rãk¸€J¸R­ÕHVÙýv‹b+µZͶ龥«¼»|F Òš©ñà2ñ™MF€+ÕZ/eM*Vá5­YÍŽ:7ê5réŒ|qü¯P…\ŧ)"­ù«2Žæ”̓^¤›yíψ™*gÐWñæà¤^ŽZÍGaæ7'­mÒBPfÎq¼`9•"¨€SÉܯxÛQ ˆ›`ü¹ åñ…±Ý[U¥Šmt,BDÝÇ?u-W{F¯ÉNUèégå…Vá)ê½é´¨Ì3 B^ °¬va Ð"–n˜JãÔÉÌô2¢“™9ª¼žÞNýÔd¹åÁ€+ø°kþáá*Œªy'K¦P*ˆøúmÕòEƒÉd寶bo42j"â lq®Ò:ŠFæÛ<±›n¯Æ»[:WmQ¸NµšÏ”ÊL¦Ø¡Øfr³õå•aÿ݃Ÿƒ…‚©ŽÕB•t­¸Îzú-Du¶t®Ü¢ xš;Å®§¹“ƒÏb–=ŽÙ‰UTª˜iAw–G`ª’£>‰È?ˆàJ ÌÝÒZ;pÖNZHgóîi­%£ÖÚ W²Ê©¨Ý+_ ñÿ²¨™Îf÷îå©|A´óŒöÓÂ"†î´eÛ¬ºŽ9Ÿ²V¥o%íÏÙ0)vºËä5©|¢TûB”%Ý- ®*TÛ’…DèE¿)D¥’¬B”Ú: ¤-ᤠ»ò0D9ËlÛ?—¯ù!.o\VѺ9´†ÅAÕÎ4º»¬YDSÈÖÞp`jTÍ3ž…`]¸ÿÅZ¡3Û5ª¥~ÚÌŠÝŒú: r&ÚÎÈê7«¥\¡ R«ùˆŸ…¥†) KÁ?s5/5E´¦-¯OþzRã„eN›òtPC|Îk­eõÍ&³Põkîô(b&i-dYék<._©âY®ù½Z#kÐ îkš¤šØ—ãRhÊ×¢·³6-£ù)]³›$S•*ÕÑÜúMbêè'­WZ=ÛêÑïÌ´”©Ü€ÞO5ŒMeµžeP™Ñˆ GE¿¾,ÚlÒgSøL~ªõ­D~ó6Ì‘ÕE‹.èæýD‡šUšš•:Æ/Á’Êê€dw[D¬E¡ýAbXƒa·QÌuô?ý•DPجdÕ=F¾Ö±ìa§AGº¨£çñ·â)å,å ØÂƒ+¨ùÁ¨-oÜîRdf­ÕÅ\Áˆ” 2BY†~åëBN¹Û˜aÈàwÅÕw…pƒ²'0#vƒ .à¸ZO]qŽˆž©Vó™áiÔ!ñS)K›žþ)ɵ[†Pæ ÆBž!ŠÌèíºt˜|WP¤žñÙ¤Ÿ_eW¡À——OsÇÓZȲÏVÜw—¯Tq/áQþ­ _µ‘yZ½6jy µ\+Ó„Ð7!cÑpO5äj&§4]Ê?Ù"¬Ÿ7 A]wºTCöLUš ¶hô©c§_Y®v•Ú²ÌÉgŸ„è[áWè¼ ÓòKŸHQůÔú)?FÊ,Šén4ZÌ×Ýoô[‘æÝ¯V€€e—)7ÄO„a=ïFÜõmB9˵·õŸ+«ù*$Wa’Ðøæmr|M·{<º¡OÓƒ*¶r»ö±›¾ˆÚŸÉ¤¸ŽÈBZÍg(Ô>Iã§€øÏZCþþ”…î!úe£47/ò˜ø1‰ ¬Ä¤y)ÞMš×³Ég|šc¶Â²Ú”Mõ<Õù[Ú¡[Àr°Ë±;³ŠJ»B-äÐÊÝ#Í«»[]'~NQ-/”Êš!~¾¹'§?!ˆJìb‚;eFŸM!Såê! Z)¢ŽŽ:øÉ%C¾c8QQcæÑdR[–9ùìÓ}+0༠Ӣ߹ÙB¹\.Š È2~‹±†ƒ/ÝÙ[y L³Ud•ÕmÝ(/Z±£†9¡œåf¶õŸ+«ù*$Wb|ØiÖíþÐ꘸&VÂÖ¡Q ôô{Éå@ÏÔ³,šÜËU½~¯!¦&œGTJ±Q‹u©Ò2Kh*O­æ32ñS³Ø¤i|ñà畯S;¥Í7!¯?õºè JBÁ§ß(x¿$•ÄzŽY9{‰žQG SÎ/ÌL©Íñ¶§¹CZ è”–ÕÇòèJ*U,K6¯S+A3W}“Fú¾šLµµ%?,3^4V Þ:к¢7­^¿« Í*´«‡ÙEasd›˜¾p†Kv‹žˆÉŸš#Ô}tÐr"ÃÐëM£‹¹ÀZ Û‚”–G¦Øê0@ߊRÏÝ0‰­2ЙlÔË$ä‘5½9Ɯ֟GÌ í¶é§¥;ØÏJ÷&ÇV’U€·^Ó;hƃÀ(Š>A­œå›?•Ôü (Wb¼ßÄ`ÁÙ2ãå¯vS£9lÙÕ9ç*!…¤¡Ó}x$vÓoWq¥ÜÒyÍY¬ä[¿A§!žC£êd'ÒûÙ ÏLKÙLÖ}ÊfôAkw‘èOª©* ès«*Ÿ’I·³ri‡øášÎ8gäÓ:ø±å@¿cvb%•*feZÌU 0Òi…mB]¨×"p.[af/ï.†ÈÕ!†52…Õ~öÄ0 ãz"ã.Èj¦üÅãY­B‘£r3Î: ñ¸gʲ‡Ð4[Ño2ߪ‡œ>^¿4oÃÄïHÌuTfîgý†Rí «,Ä,©ôOɉÙÜmŠ­Àú /$´œn VÒEƒz)WÇÊs9ÊY®eþ«¨ù@®Ì¸³`ÔžÍIT¿Œü?¥YÉ–[M\5-vƒß˜´|" ª­9¥ge>5ZáN?uùì×Q¿%»‡ É·YM–‰`òÚ¥HÊpÖ ÇδÏb±(¹ÓRDîÀ©Ù¥ÃÁБñi˳ŽÇîÈÊ*UìJ6¯C+CcñÌh2c§–f_´˜L‡˜O¦pòÜ"ßœêëôÃ,ϲ•ÈK¶Ÿù=µk4³‚ß·•r)G{…®Ï5¶™%úÔÐT/‘ؘë?·r}p.à0­xW¾°ãšÛÞÏ•Õ|„‘w2´;4‘JûÙ t W­Ë±•ʵuÃ:[­¢UµJU°U£ ݨ‡ ®Îç¥-~ÆN!0ߥ LµŽJoXV…€\‡3óÔ\…—×:Ñs]1o¢@Ë#ÓÝXmUuÑ4¥ˆU ‹Ž}\5¯×ãñc˜Ìíņ'\UÍW©q7G«Û1 Ã7þ'°GûÓ¯-ºmÖç°gÎ"°YE—f-•jœ] ÷V…Þ'*äJ¾7)«=@3çZ»ˆY¤ ´<ê›Ô¨N×·0dAãÑ%nSO&ѹ(Ë«ªùÊBGj\™#Üx¸R1_ɹ€J"­ù‘W‡n<\©¯ä\@%‘ÖüH+‹Ã7®TŒÀÆWr. Hk~¤Æ•ÅYçÁÕˆ)¬Óã#òõ:mè:´ãù\Àu~³+•[ʰOF >·¹DZó#5~ü÷„–ÞŠå­þªu­Çïê<Œºîš+zëx:Tö<Ö’fÃ+Õ2s€ÄI6Hk~¤Æ±x(@k`AÅî¨B^d1HM/Y¨V*J’…ÍÍsLÏH2ˆ—\,ùbÚ´ ›Þ\.‹Wå©°ÄÓGþ=âJ¹ÿËg)ò^Ïï$Uç™×qùl• ±9ûj×¢nˆf5×[‹Zq›O­Hk~¸ñA×ì‡×/ç]*ÄÂlôNQÖJæoTZX׺€ †ˆÿ@¯jy…­ðÔ7 ¦§¾!ÞÇWÊ‹¨o–Ú2^ÅíUÄ»aÈЧrB6­ y‘)»‘.Ô†ñ4óm"´T Ï°ÒñI¨”j†x“D­b€ mõr1—ËWj5`í²•"ñ´ÞD®~ƒ óhÇÃ+Õ£ÙLÖUK#Xaf•,~ö;—du«4›¤j«÷EfT¼¥¥:Ôg4t”‚¤ûs84ŠãF‘­¶Ñ†¼Niè,h9YÕb ¼]ºæ‡ajÜB"E¬‰I{Rÿ‘[¡¼S@Xz“=¥÷è•ñY­ÔeNA …ø˜hƒED¤…Á^ãPÆí’™tÖû¦¶¬T9‘טN'~”ghvÇ>#•ôYŽÒ”>Óµ6EE &Ûê·êå|±Ž¨ô!AA«U¡Ö•ÒD(+ubD;˜HO" ÀM5Tœõ|UGB+Õª2‰µå¨0J% èwàN7mjzéF„¶—.@ábRåi „RR j¨tðCêm¶,bîATÝC‘(üô4t´ë:°Î-_óCp 5.bsQ´ °M†qF"Ä®-C‡`$ôœpëÓZ j¸¹R8÷±°ŽŠÐ.RòûÙ ‘”q­¨>§,Ó°hVåĽÐ2ÛíNèrŽmʲÚg²ã/‹cW­b`“žÇ•°¡[© KäCåß,u\y´ÐJõh&vÕ ðW ·’ÓÈ]¥dáÆAB½ ç¶Å…j£Õ"ÅV«…úë˜[ª³!Ó:AnÐMšÑБrZú씄¯€ž†Îb–V)¶ÂÝÔü`œ”Ƈf£RAÜ!Ô¸P¦x'Õ†T ˜µ$jš†ù+4™<¥&CA–TZ jÈÜdÏïËŸêyj*n ¼È¬Çã#S–éë¬Ê‰L>Z©¤ér¦nl"`´„Ü™:‘Då³8áô0>;ÎOÙBQ*¥ˆ©ÉRÛ"!˼'æ2¸:±°FÅ)4Ü+|D½«¬TQg+û+@  Â(•,[™²é[µK o5R˜3Á$I íÕÉÕÐ9TºA·€(r" ¯d+š`’ZkÒ/`QÀ E,“9Þâ…À j~p”Ƈ˜/K% ÍçR zQõ Em1ŠÏµº&ÅéÇȤ‹_b91 ³gµÔ (µƒfý=?æ0µ)Öû–exêò"EWYî¨TN¤1ᆉ¸tk®ÑÕ4[)}tšŽÄì^UÓª¤ìŠMpz¶)¢× d=r-C°@¬¬€æH·ß“Áíe wub‡™@ÜLHÖÛ=³]«Ïçx ^ œPVª®O~ÒåPVF¡d!fsµ‰öbV¹ƒ‰A oR:*7@@ g&ôÙì~Gè–TDDsðD@¦5t±œüвq%X¾æ‡@j|Ž™@1Û@}¢Øè¹ ¶‚.fÕZýE5½¢ØÖù’Ć!º9ÅñÚ~EŠÔx³Íø#0[©þüÏÿôÆïbqhëÅÒóÍßâ‹øä‘=œEà‘MÍ^(ÞaT¼¤.GÁÒ²Z¼fÖ?õ‘ ñ Å„KÔ¶#;öBÙ‚™F¨7´ '‘&´æGjRÜ5±‘ÊϨ_¥ÈsrK—êˆ¢íÆ¢Oåš-O™–ÍMž­!±çs(öꈀè©>ŸÝÄ©ü8æÍ´×Éû¾dÍ/p¤ÆÃ³æ³›ŠÀ‘•Ê›!ÄŠ‹áHŽ*uˆZªCô–™Z»gYC£A/ÓËØû"†g±K‘-ì«´‹.ýÁ€‚ ¦õîû2ô…O¶cœXXηz쇨í†VÈëÄ êMmY^ç5³?ìwtâÔtI¸N“¶¼®ˆáf3²\Ù ¡EçF]1DKæŲkàš|mdÈîA¶ ï: f(~£7 ±•Œæ„Ìua.SnÙ‡2^A¶ÞA,Ò¡RbI»'A£ä2kY„A ÄF1•.6°#q¶Kæ?¤˜—Pp}¦tÃ!EUó¯[Ü.]óÃÊ©ñ°ŒùÜæ"0¥úúëÿ±‘0Ì@PñÕ" jÅêóӕά)ŸI$¤Ë¾!Œ|äÈîˆ+U‰Å‚ŠL©Ùn5 :Ò„æHÛì+&!'²ž´l iHkÉúz>•†¦ª³Í-\B‚•¨hÔíŽÕGÈçTÖ†-hñ¥›A¡·)7aÁ´ºµB¶Üò¤"FbÔS, …b±SadÐ… U×Qù)£EIB4¡àCZEÞ6…°E¾UÀ£Ã&x2NɨžP´«* ýh0òYO¿!6d[¾æ‡©ñ|ùÔ#À•jyÔ" ès»ÍbVHhP"f¢$[yLd7«Z[Œ1d‡){TS[µÇµNt¼¤ìãn^Ï"1 †èѸ—Í~NYâ޾ÃVãN¾×ª•ŠeH­Îš™ýý¢ZPƒÖ™[úò ®Äòñ‰ëɦ=è«Õ“¾ÌÔZh%we¹¾;ý¸g*+ÕW»]Åò‡T:_,—IÊT‚YM„ˆ/ÒP-]Ôš,q *´ÄhÖÈHLH"±˜üìËÔÕv"5¡ˆŠ«³š3öÄåbÍI*_ÁŒ\·ˆ¡Ö¨-Æ­Ùb­Û²’x.Õ$*‚P´Ä’|zA5cä*‹¥KxF6@bº±õU¿%|ÎPI*Z….’.ͺœÈ#+ªùê²3[­°—fSf«HÛ¬º%ϵM1\B½3gåâ‡zoõ³P!–¹Æ”Ú¥½v»¦U4!ÁSÁ†§FšnôA“ªV©6ÌÖÇ0J›fZiÅî·µJ¥Ö …Øúm¾bL7ê×a‘ô}jB¬[&§ÿö.æ0òË•Êåb±ÚA HúPïö:Urˆ­°j¸GX!ÿô1ÖÈÑ:-!J§kí¯È 7,Jµ¸ŽqìKbö"­ùÌVL1+G€Ù*Ò6;wÏew;†a:3zSWíB®Ô ;›ºdί!–i¼at\šÓ>'‹3‘Ö|f«•÷ÕlÙ*Ò6çΊ}Ûr"­ùÌVL.+G€Ù*Ò6»åý!?ÎDZó™­VÞW³Af«HÛlœ;+ömˈ´æ3[1¹¬f«HÛì–÷‡\ü8#iÍg¶RôÕo¿÷ÎÛú;ú‡Ÿ¶gyÁÿQ0[EÚf»³á˜h½øÔæ{{ë¸]œ3ÿ^ÝUmŠ˜KsZ<îdÞÀ¦Íè[«_í9Oá"­ùÄVw^:I5o÷f닇5±›:{íwÿéÚ½Ïß8ó8¥Ø¹pwÜßÞw>yá:xÿàÕs'(‘³=qú‚~O¼ÖÞ—6/Üq(Ìwä³[Å”ËÎq±/2úäæin×¶k§]ãŸ;}^%û 8f«HÛìºï0ÎC‡;vv÷qäàŧEúwÂ#­ùA:íýqîüÍÛSl…#w$Q..[‰}b«wϺ¦èZ׎0æûwâyA—Éæ)‰ ³U¤mVªMÍvòx=­´¼œ¾UðÍ3ìT@ÙÆ6•#òåêY¨–:B¯J¯{ä‚ÒL—;ü¦4ñþ³/,dxâ¥à˜¼8Òš¿IluIÐÉî­Ö‡W¾éÐFLm9ÎÜñð-w\ó؉òO&|ìõ÷ïñéž0€Á×óz›XC¿*Ù-õÜ b9¶Z”­ôWO9΀I[9㾫oÝýôƒæÇîßÍ:2[…¶Y«{Ô G^Ĥdé[‰ðz^e§ŠA?ïñ–à OA†CF¥ˆåhr!æa]Ä™@Z„ÌS¬ !à+}Ša!·L^'#R×Ã=HŸN Z½äJU!šRµMü¦Rû^“½­ó-Àò¨çùÉ’oˆää¼`í„®J‹÷•6ƒþábX®´VÁÊwºq¼*‡FVW—9QÚ‚&^aut-ŸËÊZa©/Ub r€6©fæöœçBkþœ6“m[]ºõÁµ3¥Rí<‰ÿOï¿¿/Êéì“òyÿŸ¤q;>ÂÈËà´.Ÿyzgçñ3/×<¶ÚÅØMÌï¹£6â ÷Ú™±•Ëq2+?[íÞrÇwÒÚýg¶ k³V{Ž`;ÉÔ·:´ؤr–)”³ˆ'ú4Ó‘ÓºC1™èFSÄâö¬&—ZÀ VDìļ1°†½N­”—2[BPkØ%IŽ¢ Å(Kªà橬ÞAˆ»ÛB 'Ĥ8„J¯ žQi¹ °T¶nô†SC öT¦ÑsgÛ†-bQÆÅ 2‹ã£5+†e÷ŠrcdÍêé™t±ëß Ä@ÌÕ¶Õ5[#¯wmBtÌ àÆ©B¥ªQÄ\„c¤°ü‰GÐí’’gÛ@ƒ+¤A§Âj~Ð5sß@¶º±ÿfÞv.ì_¥À3»×®øÙê³ù€)õÒÁíÏ>ºúR¤R§_~›FRrDö书’8\Þyâ̅ׯ¼rùÕ×.¿ì<á b«ƒ‡Ÿï r|âÌÅó§Ac«'Nçww/œáyü=÷Bé=1šsxPé@r2[…µYµsÔtÖÆè[É}žbd 1B*· ÉE*T™rCÝÓc©'Â!z§ÈB–ô”­*Ë×ñÅŸlJ‹ÊM;ýé¿4‚`ºy£–ÒÙmx*ÆŽÎUÓ@Ä]uà§iárcød|ÅTŠ_—ìXDJ ѱÚ/#‘+_”˜ ¹uŒöWµ…Õü¥óØ<¶:ÿêů½ð8}árAìœ,}&8⮳rï©}q4ßyëà ¦„µ|Îs«'ž9wéÅ‹ô÷³rõD[Q&Øvvk_|Ùº´Cû~¶Ú9{ñ+¯½N©]ùð¡K‹É',f+e›mWË…b©DµxArµšˆà­j¬IÕ·¢²L©1†Í*4¹”^†^«ÐìV¶Rñg›^÷=‡&iÌ{ƒ‘Ã6Ô¹¼™À¦&£¬i/øò˜Âr‡/‘ÏýžA3šþÇ=Bn‡æXœb+ÌÒÄb±Þ·,ƒÖÕ ¶²ºuMLke¹3ž¸%@tl$âКf¯+tÄÒ¥&cÑË QÄòU\Ѭãq¢Ÿƒ =£¬ù¡W,prãØ õÛc¯Óª¿ÖKbþM³Õƒ÷_Gœg®¾+‡ ÞqŽŸ|aÏ8ÜsÖž(ì{l5÷s+iÿ›·ˆ†î̲ե[›COSã,f«Ð6KIá3‰Õ·=Ï4[a•…á[e‘ $rb•Z‹Ôä:T xÑÓŠ È)‚î‹$É!;9oµ@®æ>@‰¡^öd¤Š5¹Ê"HÀKšù¯¶l7I‹¶lŽ8q<8êW 9Äk7拨>)†eC’lºªÐ%]Œ ‡FEü¶Éäñ+¿nJÍž ˆŽ‘‚€à0•£1¦2±›c¹^“œ•JUW±â=´æÏ€¼àÍd«Ͼ'†*·^ôÖãÙê‹/?8ÿ$Õ ±ùßÝ]2øÄés—vÏí8iR—ßiylEã#9üqG[!c+— l•zòéÓ§O9'Ÿ>{å­©>?¹_™­ÂÚì\«,d3N¦¾Õ [ÉÂÈ·´¦~¾ÓÊêIM.71ÆLî:qˆfSÙYË4Þ±naæ ò›Y//Qxy¶&wÔ–1-IÉLüêðÍN^üM!†åK @&תPì2)AÎl ‰Ž…&Æ’‹I°gòšû@XÍŸÛHPÂMb«órbsò•‚P>s^ƒÂ!<<eô—Ý÷ªN¿&Sº¤Ÿ—ëË]žÂçîu1øº·'—dÐjvÉV¾#wo>/®¹¸ÇSb©¡H|GúF×zg}Y`Ú’KOSž3[…¶Ù¡at§:í æ¹Ôñõë[Lgà æÊ ¥†ª×>4°¼àh¥ûGÁ2в݃¡GRTˆa…ºÖ©•å=ßP§O†ÖüÀ«æ<±Alõù‡·öönìí_¿V{ß 7ñðvíúµý{{×÷?ñ?º§#þn}ä.G?-ºÿÞÍ«—v/ìî>¿ûòýSoùƒ×ö®ï}è±ÞÃÛâÈþ‡XÎ×~íݨQ.Îñk·>öŒÃ&|×Þ¹'§þ®_;xß%Á±'cz¢ƒ˜e¶Š´ÍÎÙ´ñiíúV£¾ I-HX͹Ö`&ì5˹" ½îè“Á–-ÂÖXÅÜÙÑ^$)E¤5“Ø*æ}øö¸Çli›MRçžn‘Ö|f«í!‘µ•”Ù*Ò6»eý7IDZó™­ÖÖ‡oOFÌV‘¶Ù$u^ìë–!iÍg¶ÚY[I™­"m³[Öÿqq“„@¤5ŸÙjm}øödÄli›MRçžn‘Ö|f«í!‘µ•”Ù*Ò6»eý7IDZó™­ÖÖ‡oOFÌV‘¶Ù$u^ìë–!iÍg¶ÚY[I™­"m³[Öÿqq“„@¤5ŸÙjm}øödÄli›MRçžn‘Öüjõúïìý6ÿ1+Dàúõ½-k£ÓÅ}õÕ˯òÆl#—§g#píÚwŸñ#°m|W{3Æí’]ci®]Ó¶gâ—KÊxhÌVÓgb³•×}ñÎV!Àl뎉cf`¶Úª.š ë!Àl5ÓðF Ö0[yÝïlÌV±î˜Ø9F`f«­ê¢¹°ÌV3`b³•×}ñÎV!Àl뎉cf`¶Úª.š ë!Àl5ÓðF Ö0[yÝW„;í·Îž|êäɧ'ÿž:yæ•»_~a¾l<f«XwLì#0ƒÀÖ±ÕÃÛï½ó¶þŽþá§­õÑÄÃÛ7÷j7÷§ÿôÁÝ©çÞ'ëºð¹yï¾wp¼q‰ŽÈ}ÿÇ®Æ)1³ÕLgÀX#¶ºóÒɶݛ­/ÖÄnêìµÜý§kwj§é|ê™Wß÷mçàÉÝš8xÿàÕs'D2ùï‰Óô{bhÓÞ—6/Üq.÷ùìÖE‘þéÚ½Ïï\ ý“Ý1Ñýkg§#;päîLIüÛÉ¥Ša”cПû¹qN¦¼ü> Iœ®X^ë‘Ö‰ü‡èüÛû;âøy¯ëvÙ G\BA..[¹¹|æ:“zR²•à2™«ïÿ/và®q_jìžxžåÓ½ïð‰Ç<—S§_ùLVH‰Ü²?öÆGîpì¡ÃÝ.›8l5.2 Úl%KwDîwœ;â"V{á)éìNpacE^ÌV3`b@RØÊÜj}xå›NŽNµ-ÇY‚Gn>ïGÇ.þÜdÏ7ý=ÿÎóz›¸L¿êpÙs7nƒ­äØjÜuûޏ„2ËV·eMùÙêÉsú§·›àýÝm»”!XÆßi;Ƨ/¡!Þžµ¥»tã]qÉí7Î:Œ@"M”%zø–l¦R'¶{ìuÁÝ_|ÆV'‘ûÔå_ñÈ”Ù*Ö žc‹@²ØêÒ­®Áê±'ñÿéý÷÷EŸL<òÅ÷åL”CRîÇÉ—ßBWï2ÆÞt\ëò™§wv?órÍc«]ŒÝ$¡¸óAc+ôçÍky7o&PŒ­v.Îó4Jfä8æNý9¹Ã·8î4¦tûÝ3"K ÖŽ(јnÆ>ÒÞ“bÔFe e«£r÷]b½Ù¥FäÀl•ØÎ€gb@ÂØêÆþsø¿saÿ*1Åîµ+c¶Bÿ\xœ:äy½}_¿ò,í§{ãcêçu9"{ò³fÿÜ‘Ôg.¼~å•˯¾vùeç —š­v.~ö¥.Éñô™S”luâ©çv/œáyñwîüË{râN‘¯G£âò‰÷œÉÉ]ïQšp{_L¸=QØ;¢D_~þ™ûí¥ƒÛŸ}tõ ò2uúeï¹^([•»ÇVà÷ò¹Þ‰SçÏÒD"³U¬<;Ç$d±ÕùW/ {>¾‚ŽñäÙs'iJŒ­Ð?ß’´ßo¾/™kÇY¡¿*æÝçM¼pÙ ¶¦65[¾xíeAR'_ùPfêg«)©s¾uÓ\éÎÒc¯ ¯Ü%"»7Ý…"|VuâÌ•#JD‰u9{æêÞ‘Ë?¿&¸[dÊVGåî±Õ¥›oÉéÐ3×ô[f«Ävì8#kÅV~†8â V~)æýœµ[}ñå»gÅã“3/¿vFìÕ ÊÝc+iœå’Bf«Ävì8#k’ÇV'ž}ï!õ··^sqÔ]ŽÙêÁG¯É1vWz‹®þþëâ?qúÜ¥Ýs;”‚¶Ëï´<¶’½.¥wçƒÇVº å_ÁþøÉÓ§N»'OžzY¸Œ3µã¿|êTóº³j1…ÁÚÕ«¯¿è-ê8u‹–5†–ÈÉîƒóOÊRâ¿DéÎëgO ÷ž–O²Nì< WO|뎳‚],ù8*w—Laز1[ꩳsŒ@ÂH [÷Hèä+rbí3ù¢.uÆûæ÷n{)'çÐ@@úy¹N.q¶Ýëb¸qoOvÝ´š]öó¾#wµñ”‹»Ÿ’K ‘Ø9"ÞòκæÏ³žÙÎò_>ÅVà£ý]‘=“ƒVœ”!%r3Ò_¯PáêÓ¯ ÜÆøxåÊ^{ANëÉuƒá¹ßñpC]úý /ÇSÅ™‚ÈÑ\¼þóûV ïºØý­C !lõù‡·öönìí_¿V{ßí«Þ®]¿¶coïúþ'b´%;É»ï×ö®OtûÏûïݼzi÷Âîîó»/_Ñ?õ^ƒºspã®úÐ{ª…XItdÿCŒbÚï"£½5äòàÓ·áÃDŽ÷ÞÅ‘ý›oƒ ÜÓi~úþ&̺$âúã”—{ý;wßß»ôBþÌ™gÏœÉï¾|•ü™0T"7™téúµ[ÉñÒ $} ÷€†€îÚþÁxÂ0$wº#(ìód^ÉËk︌?á§ëOl2[m]gÇN8Ia«É.:v]»—8˜­Þu±û[‡³UâºYvx%0[m]gÇN8ÌV+éúØHâ`¶Jx×ÅîoÌV‰ëfÙá• Àlµu8á0[­¤ëc#‰C€Ù*á]»¿u0[%®›e‡W‚³ÕÖuv\à„#Àlµ’®$f«„w]ìþÖ!Àl•¸n–^ ÌV[×Ùq޳ÕJº>6’8˜­Þu±û[‡ÀûßûÞïñ#°mìïïo]kç3Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À0Œ#À¬ÿë´'¥ endstream endobj 67 0 obj 27840 endobj 69 0 obj << /Length 70 0 R /Filter /FlateDecode >> stream x½˜]o£F†ïùçb+;Rð2|;UWjÓ­´R›UTÔ^4•5œL‹±xÛ(Úÿ¾g<Œ=CÖÆ´ä"1&œyÏ™çÌÜÂ8ø3%.„sŠ~‡Þ^—âR|å@ó§&ÄW}S~ ñ׊WðCÄå/sÄo›.¸¾Ñ ÞþäOð.DKŸ$P®W)Ð$a[ç4ƒM±Þ¤EÅÒîŸá‘æÉd2¹°¢¿à}Tû(í òSm:¬·¶­Úö;ˆi×ë¼Jój²Y]À¹&lBfÖA„ÿMRø-Í–?\]5¿=Þ\W>m×›&t[¦PV‹«! xÑÀ?´È1–r~h4Ѥk !Ñ,Ä-KmIà¥YKT‚ÔÕ)*³g{kbq‘Tô>K9E¡÷F×o¢÷7Ñèòx  ª-SMÙs}‰Ú÷:“ÙÔ ý€ðšõB7ðçàXG±©Œf ›³Æ¬\Ð{Ô+9-*c§°‰j•PnïÅʱåó[O%Þªž7unÕ|Ö­çˆ8;‹‹¹¦–4–ÿhsÜ·ÚÄÓ¥ xÅuOZ°„Çø¬±~ÀNõ#ø|y~;´‰¯(µi‰²zA`X½G*Â8!qµ-m#±ÉTïéBc©.ÈMà—ÚQU±ýU47«¨uAýK xÔ&k E¹ŽIQ¢le WÔ”Dé¿•°ôîÛY±.1õB¹$‹l>2F-Õ‰a+ËuuêÒ~¼*ŸgÛz´¾¦SËæÜ…œ hU—…›R×¥+DIÜPK˜5Û6UÃÆeEŸ†,oŠIPM6«"[3&Dˆâ£¢ßî d÷Ô誵ø†?¾Ë„%¨ôèýÆàž€cÛz†¦9H?vCS?NhEåz[Ä5•Èæô#~ñ«¸uejTÖëøßáL©‹F†ŸO ¼îÏçZêÈ/ö°~76°üÔ™Á pž² CËÕ€ú.§á¶5î52t®…èkÁ-ô˜ÐFGÊ HÏm 84x®Ö¦–Ë‘nû·gawŽãíY›>Üœ‚ ä#×jú«Cß±'no?:À‘A´QÙqš™XƇØù Õ¯éÚRÏú|w.‘¡õ\r€g‚2ºã|Öñ±³z½wb”o„ëÓé½Û€ŠÎÍA‡‘5AŠ ¿w¥ïì³ ÛW™YA4éÂŽt¥²<ÂSJõˆ©ÎD*0+‰àÉØ»Z$“"Ä„íŠÍJ.^e1¬Ï~õ`±=“Mà;ãïa˜ ÙÀ_ç+!è€õO4Û¦‹E¯zÜQžÝ§ÂÞĨĮ¬Im)Yg-¿bšÅÛŒVébY¬W¢¯ªþ´óCïs[;PÑù5b ï¼áŽ<Ý8¨Œe«Sr±W!í‡rS°¼ZÞGo¾™¸ËÑå%wpwÇ'Ø€ÏçvŸLç`s÷´= ­þ0Bû ó@€ ®¹ ÿû< ¶Ë…²²ÆƒÌSÃ9¼:Ü~]kŒ¹ endstream endobj 70 0 obj 1174 endobj 68 0 obj << /Type /Page /Parent 50 0 R /Resources 71 0 R /Contents 69 0 R /MediaBox [0 0 612 792] >> endobj 71 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 73 0 obj << /Length 74 0 R /Filter /FlateDecode >> stream x=Œ½ Â0…÷>Å7ê`šÄØÄÕ¢{ à\B´)Ôøþ˜TðžápþîŠgEV JcÏšwâÎB?E,[$)±µ„¶íÜÏü+[‡Rw1s (c¶¸±’N3„L3B¢vã<-ÏÄgNõýœò´'¼¸|ç¿ÜŠ"½ endstream endobj 74 0 obj 123 endobj 72 0 obj << /Type /Page /Parent 50 0 R /Resources 75 0 R /Contents 73 0 R /MediaBox [0 0 612 792] >> endobj 75 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 77 0 obj << /Length 78 0 R /Filter /FlateDecode >> stream xOÉ Â0½÷+ÞQÆ$îB-Š„€GIcÄJZiÒêï[¬Qœ9 ³½%Ç9¨Ï!ãM8¬Æº‘cP®ZQ8õ¼"ŒVQ›nä)TйãO0ZÕpð~"EwÙ'~ qB+<!‘é;ÔÕ”iFiC\°µš¹‚jºß<,.7I¡g¦Ð…ŒÆ&ŒVëíÒ+¨¹á´M¤9deû³›´ê,í´ü«_|w7ÆƒÚøKR^&ÅÛñî˜E]x endstream endobj 78 0 obj 201 endobj 76 0 obj << /Type /Page /Parent 50 0 R /Resources 79 0 R /Contents 77 0 R /MediaBox [0 0 612 792] >> endobj 79 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 81 0 obj << /Length 82 0 R /Filter /FlateDecode >> stream x­–KOã0…÷ùw ‹¦±ûîHHPŠi@‚¶3$dRC2“8©í€ø÷ãØÎ£‰Š´]TIÛ|>×÷ßÜÁ<õ# “Ná0è/_è¯<~þ+yúen–WõG;~ k@8˜§?{h„°Ž¡5tÕ]X?ÃÉùv }?‰²˜¹®{ ë¿°\›Õ”OÖ*¯>æ´(b…’ž‰$å ÉSDáæ|ñóúv D­À°AP’è‘eñ“úÙ+á~@øS§ëzà€îBÓR¸c„KÚe¡ì®ø a0š:ÒžAÆ!K·DRð#"ÝAÅ"»…êSoao8ñJ)v79#d/ÀHLEJ|:‡ß”mÕ­ètÚËBY 8šV@[»«$c[PP"ɸOÅnˆ„ŒAáxV­ÂE@ý¹B+k>¿Tð•fÏç Ï WîhL@5{¡"ïæOwO«S¯Z­À ÁewWnJí 1‚^d•¯öôtg쟙¢6Õl'2¾ðÊ ,ày*‡ ¤­ß=Iôšï¨Ÿp®š6Ñíj|R¿’Eµ¢à 4¨ðMÇX_î§žóqºt ÂÃÉ9)½©Iò$¥\†¦5;­B:y ·Å F ¤Št¨lRXÔ+ ¢ôEߺ«åýõù¯ÇÛÍÍÅòNlì>œÁ9h8®–WX§H+S‘ë˯å`‹£Ñi¨ w™Šå„ ÉIÈä‘¢ÛiÙhN#•Ö „©¦vî‚Â'íÈ\m¯˜J¢ƒº–ˆß?Ь‰ÆÒTæ&{u«¹åh'öÚÑUÖVžÃˆŠw!ilýüÍ“çT#¸6ÐO‰ ú2éÛ³¨o=æ¦ñâãvZ]•ºÊq"‰ÓˆJªgµïªÔRËzt‘Äq(õLQì¦LöüýÄì8¶â Ûñµˆ(aùÉ¥_EÛˆQ-‚êñŸ»¿VÅÏM¼m̤f|‹9ƒª!îþ >ÔF endstream endobj 82 0 obj 759 endobj 80 0 obj << /Type /Page /Parent 50 0 R /Resources 83 0 R /Contents 81 0 R /MediaBox [0 0 612 792] >> endobj 83 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 86 0 obj << /Length 87 0 R /Filter /FlateDecode >> stream x+TT(T0P°´Ô³4000R0·4R014R(JUWÈSÐH-JN-()MÌQ(Ê+Õ514Ô³TЩ4«7 p%ç*è{æš*¸äÍ ʲf endstream endobj 87 0 obj 86 endobj 84 0 obj << /Type /Page /Parent 85 0 R /Resources 88 0 R /Contents 86 0 R /MediaBox [0 0 792 612] >> endobj 88 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im5 89 0 R >> >> endobj 89 0 obj << /Length 90 0 R /Type /XObject /Subtype /Image /Width 722 /Height 1388 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xì½O«'MvçWµñÐ÷ ùxqA㥱Þ®bÆŒ¼™Y¸@0Z0ÆžM°¬-H©ÁÒèiIãgÕíê~ðâ駦ZÏ”«ü‰<¿{**3ãäÉùïžK’72"~‘ß8yÎ7#ND~ø@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @þð‡Ÿ}öýßýÝÿ£ùñ[¿õÏ›—@ <¢%h—ÐϦÐê0+Oîúµ?l'ã’¨É_þÕÿý7ûWÍÄãW¿úÕø~q@ °ð nûƒü “MÁHýËù¿‡YÙ »ŠJ°Ž z0nÀît5%Úº` Åy]E%XÇy#j€®¦D«¬C¡8o «¨ë8¯`DÍ@ ð#ÐÕ”h5‚u(ç t•`猨y ~ºš­F°…â¼®¢¬ã¼‚5@À@WS¢ÕÖ¡Pœ7ÐUT‚uœW0¢æ@ øèjJ´Á:ŠóºŠJ°Žó FÔ<?]M‰V#X‡BqÞ@WQ Öq^Áˆš@ àG «)ÑjëP(Îè**Á:Î+Qó@ üt5%Z` Åy]E%XÇy#j€®¦D«¬C¡8o «¨ë8¯`DÍ@ ð#ÐÕ”h5ü¬ƒÓÿÝ_þ®ž¯rÄŽèŠs×@WQ Öѵï¢ð@ ‚@WS¢mt²Žó¯~ãüÏ^ü·ÿÉÇã{ÿù‹ŸüÑÿjs`Šs×@WQ Öѵï¢ð@ ‚@WS¢mô°Žßÿ_þë/^Î6ñÖ¡8w t•`]û. @à t5%ÚÆEÖ¯¸ñûWÿø{ð?ýî_ýöþůýæŸK$ Æ'kƒu(Î]]E%XG×¾‹Â@ 8]M‰¶q‘uüÖoýóD0î_ýàßþM>Ÿò½ïÿPˆÇŸüéçñy8X‡âÜ5ÐUTY‡8üüèÇŸ©ÏÏ(‹DF´~õ«_uE& @ p"ÐÕ”hYÇ?û'ÿvÈí…„¿û_þw$QÏi’ÄëPœ»ºŠÊ"ëøì³ïç?ÓpI<‚ut•Š(<Ut5%Z“EÖ!c̤Œl‡Î¼ÄX‡‚¹W «¨xX‡ŒzÝÎw/ô`”ìîa$9z¬c/‰û@ 0E «)ÑÛ-²¼8Äš°h…‘s1,iá•6Å—m 9c¬Cqîè**«Xì”Á1ú ¦ääPš1 ëè*Qx «èjJ´&‹¬KñßüƒkÇ‹—dÃ!s+°8ÉÈ”ä—Á:ç®®¢²È: ¸'!¹•F9†åNRhêÿù?ÿ§Ô\$òp°Ž®R…@ ° ®¦DkâaXŠ´håî*‰ÕÀÄüƒø›r3X‡âÜ5ÐUTY‡ ÜJa ¬r‚„(ý€„h†Q XGW©ˆÂ@ X…@WS¢5q²ì6EŽ‘í0.›°jH9¸,6?cõ0©G8´;žè**NÖ!+Y¾øƒß`w—ßûõaömô0æà‚u<­»ãW@ ô@ «)Ñ ûYóõŒ™ËÁô=Îìæ´zù)%ÄêÕwt} *ÓÀ"þÈ3)ÃÍÕgðöa(Œñ1Æ=ý88þÓ&GL Ï®¦Dñô³õå‰{¾ç%·dVšŒu,Z½ÒÝãƒu¨Eü†R‘@H@_ØC¥Ž8þFÛ#)çƒÀÑX^¼ºê‘æî‡!ôÞ+g­^ɨ-ÆÄêÕwt} ÆcµˆÎ:”~äRG£í‘ÀóA «)QýcSÛ!£1Ö¡`>-PßÑõ%5_dˆPZ½R>¦’#1Á: Ø#)èjJ´-~Ö!挜˰¹|î¿ø¯J6…ø˜aQœ@}G×—`To‘uÐÑÂ<9ÿëŸþ•¶ì0)Ù‚u°GR #ÐÕ”h[ü¬cÖ¯ƒtµ2Ó@°ÅÙÔwt} Fõ<¬C»·RÜŒ¹ä,[£ÃN5uÖaÀI@ lŒ@WS¢mYÇ:d§ëa‘BÚa8F¦$¿¼ëøêË?âPèZê;º¾£-~Ö‹wùvºvè–¶¹lÖaÀI@ lŒ@WS¢mñ³ÙÝzz™’üòb¬CöúVèZê;º¾£-~Ö!+gq3fÞ Êû±ìj[ò7ÖaÀI@ lŒ@WS¢mñ³ŽÜ¯C¼;äœÓŒQø:¬ãý»±ÂÃ¥bX¨ïèúŒ&øY‡ n ìÝÁ|D?îJÞÁ: Ø#)èjJ´-~Öñlý:¾ùùOä«"⨠ D*†•úŽ®/Áh‚Ÿuˆ„àÚý+ þÆÁ: Ø#)èjJ´-~֡߀KîÙ2ÉÑøF~y±ŽÿË¿ÿí¿~ÃU ÿôïÂÚ@}G×—`´ÁÏ:>Jˆì…þxÎE"ë0`¤@ 6F «)ѶøY6‚wX,¯¸vèbÉÜŽŒÂ×`ŠÐ"½l¨ïèúŒ¶øY"Á.èp=ËWhGR¡—Á: Ø#)èjJ´-~Ö!³ölD©Vc1p1ÖŸä÷¾ÿC…®U ¾£ëK0ÚâgÈă+º© —ñÛH @à8t5%ÚL?ë½ÐÙúIɆ wèå4p1ÖñË÷8šÿÕwt} F£V±|9d%‹pTûû€1ÖaÀI@ lŒ@WS¢mñ³ýæ,¦J=¦dCc.Æ:´¶úŽ®/Áh‘ŸuÈvµ‰oè¾./^"-*£@°öH @`cºšm‹Ÿu<Û5,ŠU§@}G×—`4ÍÏ:d”ƒy(ñüapLFô¾„‰,ÉIøu°$éW¿úVqÈÏAd#ªq=ºš…ËÏ:°¸ 2â¡Çïýúí­¶dV‚u(ÎF ¾£ëK0ªçgˆ“nPSÜ `P2ë0`?H\:ô oúϹ<2ôçƒÈFTãzt5% —Ÿu`#äí5?³°…§ t<_Ö±æC-õ]_‚ÊÃ4°È:X@—g.‰G°Ž)ÚG‹Ö–îý}q„^¸vºš…ÎÏ:˜¯‡o°ó$Ó÷ºBÁصãJ¬ã‹?ø}{ÿ&]–ÿò¨¥¾£ëK(7åÃ"ë CND§á`¼O Kwœо8N_\µ&]M‰‚ægbM° Â:0ÜGdÉW Wb´” ¦¿ýßþ Ù˜B1œÈÌ<9?¼ý|š:Š©ïèúFUÊ/Y"”dƒ=:?¼r Ë®wÁ:r<ÏKwœþо8N_\µ&]M‰‚¶‚uðÅ·Áò2èñàÃ÷bežëÀ˜âR+^µ´]1œ$'_!{°”ã?¾þýi©ïèú´2ÓÀ"ë€TÈÂBu¿‰Œѧž(&,Ýq:+úâ8}qÕšt5% šŸuè  .öEÜ< ¥7ÙKu¼C«ÿõOÿƒàöíû|yV1œä‹´lŸÎO„›¡1¦Ù$¦¾£ëK(Õxë@h¬1ã6•ðë0`?HRXºƒtÕˆ¾8N_\µ&]M‰‚æg˜ŒD<†asìKb÷¯Œ7Ù+±Žo¿ù {úÍßþ‹ÿïï¿áøðîuš=)ÿ‰_ùÙ7^Æ= *¥¿úŽ®/¡T7âý¬ã×¾ó]P¢íÌ@åLjlèe°öƒ$…¥;HGPè‹ãôÅUkÒÕ”(h«X;uè°9¯´‹oµWòë`(&U¾>ü aÅp 5åöqýü›iú'1õ]_Â'úôÂÏ:ð4¦áÓCiÆ(¬ãS¤x–î8½}qœ¾¸jMºšÍÏ:X$‹G‡˜] p92%ùå•X‡L—0±ÂñOÿ¯ÿF¡NX^òÈ<Ë4uSßÑõ%Œª”_úY,§¸Ç蜋DÖ‘ã|ÌpXºãôKôÅqúâª5éjJ4?ëY{ÿ›,öåJ¬C“I{C½>n32úã¹@}G×—0W¯[œ“u|õåÑã£á¯ÑeN9ë0`?HRXºƒtÕˆ¾8N_\µ&]M‰‚ægØ(o²¿ý‡‘ndJòË+±Ž7ÿï¬F‘Ñ{¿ŽU™ë;º¾•‡i`‘uðY@ƾ ãp8{FÂuLÑ>ZLXºãôHôÅqúâª5éjJ4?ëÀŽÀ:rR±¾ëÀO¬X¼Ãôì–ÜJ «2×wt} …v¤èEÖ!tddå¬4,X‡û¾I|~EöÄY:Ã)zß ?‡»úâ949Ú¸1]M‰¶ÅÏ:Äš°6ŸÒü0¸ÇeX‡¬aùÙ/¾„ÿË¿—K”€Â˜Veæ‡õ]_B^ÿQx‘uà` cl4¦b% Ö1‚ú —P†­˜£>béòp|}l¯n Ö±òÏç¾]M‰Âègê×Á‹|~”l ñ—a<ïð æM8ó˜vH»¥Ž«2óÛúŽ®/aÔ„ür‘u¨ÀE5,ðëÈ‘âÁçAæŒ7õ1ëùLj¬ã™tôŽÍìjJ´]~Ö!6…Õ‘2¦GG2Á/^RÏé(‡Ä\†u°5-Mßœ}ÿ†¶:Ç»C1Ô­&ydKR9ßV¾h¦I ¾£ëK˜Têc„Ÿu¨_,+?Jâ¬ã#ʇ ¡ rÐY‡¬ã3ªT°ŽgÔÙ;5µ«)Ñ6­eêËÁ„‹Œ¨?ÖñõÛo¡,ÇàS³rLÇ4€TX‡îÏ)ìo¢"å¿úŽ®/¡\»Þ¤0Rº†/ë0à=rÔ(ë`HóÈU}u Öñzyß6v5%Ú4?ëÐ7Yø*HŒ)Æå™Ì°Ð^>;Ë2ãëol &Ÿ‡ËÏéSqσuÀ.ûÑ?ƒ~pæ°W9ÅX‡>†‡ È Ü#¶éؽ‚uìÞ—¯ÀÑXÇíMöÅKÖòÃ4°¤Pp™}Ÿ½Ì Ï;íÅ4­dáxûù—_yç¸å·†¸Öwt} Fõü3,ÈNùÜ {›Ì †Dë0`?H’lÜÁ–ƒÔç9W#XÇsîýmÚÞÕ”hücÌ­ðöÊZHÙ£C†Y¹ ë`Á °¬_ûÎw9İ*†v€Ù~kä©ïèúŒêùY”#qÑO^“K¬Ã€ý Iºrö õyÎÕÖñœ{›¶w5%Ú?ë`†kË6¸40É’› ‘yŒ„/Ã:\ïß°÷C¸•ÚD‚¶äWí]ê;º¾­ê4àgâçó¿÷ˆ a ¥9¸`S´ƒŠ8`­ža•‚u<ÃN߸É]M‰¶eëÐ×X¡êY*ñgo?Ç;T§ìU„=¿ÓÄ@Þ~®€Oõ]_´Vãg*Â::ˆd”~*ÄëPgt+à÷8@¾G±½ËœEéùDëx>}½WK»šm”Ÿuà˜Þ^eÊÇQtù↚›‘q¹ÒX”#M²Ü¿bj‰ W §Ý8=ýäîÁÎ\ßÑõ%L› 1˜’Ò`Ũ»e%5ó,´â)ÁÖ¡`® ø‘uÄâåù^W!_Õ/{eÖ±òÏç¾ÛÌïx´p°Ž£õÈõê³ÍSæg¼½bj!ìÑa[}ú ë@ºX·’Þâ_¼¤up^ê ‘Ã÷C];ð+#s}G×—`T/lŸNפ@>‡·«ç7:l8XÇa»æ2Ûæ)ó³{ŸIeyàJ¬¹ÂƒMGù „³óq~ed®ïèúŒê…í3ÀéšÈçðvòüF‡ ë8l×\¦bÛ£’—LZD¾ìb¼ùw¯Ó¶ºß~$ó“rYæÏ]{¡«w­y«Âƒu´B2Ê)!°ÍS¶È:Ye(N–CÅ]ɯCv¢`ýÎÏ~ñuZÌrÿ :1ûÇ÷î1»ì×qcß|Å¥ñ¡ðúŽ®/a¶!¹hûn¬ãþc;,ØÑ3¡% ñÚ>£r—NZD¾ìb¼yÊAÔùÜ¡ô£ÿ]Pï*ä»´híMƒu¬E,ò¯E`›§l‘u<ùM•u%ÖÁ¢v¦AÀ^ÂÁery¼˜‡¢½_ßÑõ%Õ[´}"!ŒŒmÖa»˜´ˆüpÿ¥“u íˆ:K·gX7}+ß•&•`Ì(qþ½_¿}©‡¡ž’M\kû´JÏ$°ˆ| ØÅø3"ßUÈO!QÁ:NÑM§®ä6O™“uÈ—CQVÁ:ž,TWeظäj;»€åþƒ%#xFÛ÷äÞƒuä m£ó;-¬ãh=r½úló”9YÇt†EßpK6…ø+y“Ö Ø…Y}Íþ$É•åþ•,ð!1>8ËO‚uØB¬#Çg}˜ßñhá`Gë‘ëÕg›§l‘u ú°#2NκQ,Ë÷¾ÿCLÆuÜ=.Á:r±¼6ë@ÖàøÑ?#Ì‚kÙ>wöÉI°Ž\<¦á`9&ÛèÃüŽG ë8Z\¯>ÛÌïx´p°Ž£õÈõê³ÍSæab&píÀ¦`Pä’+20.jD¦`¹X^žu@0Ù¸{`•“„{0,6• b‚uäâ1 ëÈ1ÙFæw‘ æY0îÁƒcÈLIBÎhû– ~äK—â×!?+ê|“enË»,$©Ø¥çH³½c|Q‘_tòÇú°ŽCwÏ%*·ÍSæg‘´¹wÆ:ôÈ==¦*/ª°û»`1£ÄßäýKãƒbyÓjŽáK÷\"YßÑõ%ÕóÛ>â’D9æö,U9YgûŒ*^4ɼBê ¬Bž…B§ÏÀéñî5Ï÷š~ø"3©Pî¤b&kú*"ßx"ʄ˹\·¸®BnÜ÷8IÁ:ŽÓW­É6O™ŸuÜfíaŒx<z ‘ Ew Ö!Rùí¿~… ÌD‰|¼¾øA·÷o€…u£ìMÊÁ’R.ï¶Ôwt} ÆCä·},n¾1:—$d•í3jxÕ$?ò%„Kñ«çaG€ùô‡„^2q6E>‘‡áë |4—Ó<ƒœ°&ŽœiÙu°…f.¬c•ˆk‰@WS¢]Ë:Ð zp$­2Œ{”4ñ×`èXÚ bXU¨æJh;J@aÌēʗg%R.K™ÉSßÑõ%äõ…ý¶! ‚eåè•$d•íÕê9\ú‘/!\Š_…<¾ÁP¾ûÆA€KÀ§[§¬ƒ‘@$_¿:Än-\9ÛYò\|õå¥ÉJŽ·ŸëgówòÙ;-ÄÐ?ôéÑ*õ¹ ÛX]Jä]x$’4Š”ËU¶Ï¨áU“<ÈÏ»¹ y¤÷˯n^L¯_ÿ4=>ðÒÁ]FÈ ‘Ðçbt9Ê,‰Ž¢È@Ê(O~ÙUÈó6 žÁ:Û;רØ6O™Ÿu`>80¦Ì2ÈPc¨¸k°Y ñ`ø•ËL4sÖ ;bF6Ô©xÇ€·™ë;º¾£zÛD`BKi8î2­Ï™0p•$d•í3jxÕ$ò%líøUÈÓ§,“e'³3†B$ÐâÎ!™‰HJf²’43ŠÂ×™H1º²«÷=NR°ŽãôÅUk²ÍSægè1,ˆXÔ…Ø‹¡ß®Á:Ї8up iœyËã@O–þн ‘GFŒíÌRßÑõ%”ÚB¼ÇöÉD¿HÅô\îXeûŒ^5Ƀ¼ñôI«G˜éSñùäñÇaÉ\zŸš“‡0¿-eN É9—#Ͷ”ÿº yù¶J Öq Î¸hU¶yÊü¬#ù‰É–Ç¥\ò2k(·k°‘¯´rvv aIü˜ˆŸÖšõ]_‚QAíc®-Ù¦Áǘ€²„'X‡¯‘äAÞxúŒ¤U¬CXtêÜûWPh.?áçÂÉ Ï‰èÏè @=Ð'š4 tòéí¬ã€r±*mó”ùY‡ŒrÈ'½ðKVf !¥= PzWb.š“dLw0îíç2ãh‰±”ÿê;º¾„rí\cbà„{¨±c2n£IXeûŒ^5é ¬CÈ6ü!Qˆ¥]5è –ÙÊÀHZoëù{ÿ†œ wÈpbé]…¼tÓCÅë8Tw\²2ÛXÇ¡ºã’•Ùæ)ó³æVP T &a" ýv¥õgŸ}_$MáJÎù(À!UX‡lbp핳"˜ŽHÈ Ó+ð4bJ£a«lß%Ÿq»Qa²ùd[(7Zª¶H~šˆþ¹Ä·%³¬ ƒÀ3 ™2?þvz‹môáô¾Ç‰±X9{œî¸dM¶yÊü¬Ë"óbDxß·)ù¯Ä: 7¿Žd¹$u¨P¼(™…AE€UÚ¯cî£oúóúŽ®/A+3 ømVƒƒ~g@L6Kôãî!¼I§¨zbüÈ ëóŸWñ=z“_*,T¡TyaãÌÂH™Ž)ñí[æ¿ýÈ ~ÌÄõ^`^ªöYâƒuœ¥§Î[Ï®¦DaYd˜ Ù £t6tÝ•X#ÀºZbkÈ4\<|p°¼Ð3|¨ïèú¦µÒ¿í“98™zcÜCòv6+$«lŸÖçùüÈÏÂkDú‘šÁæ`;T× £ x.`)²–€™Õ+é1aõï|—3ù’» ¹qßã$ë8N_\µ&Ûzs%Öá_?ÈË @1O-’Ã8sïM»ŠÊ¢í#í5•‡Q ·ŠÞëÉmußEäGxú/ýÈ˺oæä}Qþx½ìT“ÃÞ¿B&%³oî0óíý:ÎË: 0ŸœÔU{<¹VñÃ]ØFü¬c­¯ :ðJ¬Ã¿~Pt/ÃË2óáÝë4.]þ«ïèúʵ[ççý‹ñ̇Œ†ñ†+®É©ãq-í¬AôÛ>£zN:ë5,L²Üp5‘³°ËbXX7À³O ”YøLžúåW}? uÖ¡Pèª=òEøøl# ~Ö1õã2kP$ò2¬Ct¯ý ÌªÈŒ Èè^CÞê;º¾£z~Û'ž?l&½G‡Œ±—F³ƒu°“äGÞxg“üÈ3R‡ôB¡¥ª²S±—™YbÏÒr{¿f $3Ù êœyR @ y°Žç†ÀæÅFøŒl# ~ÖúBçÈîÖLå³.ƒC.g5‘—aI÷Þ=ä=bû磖^fbå¶ãóØ%Læà •ˆ°,²­œFjÀoûÎøüÖ×ù¬ƒV$wŽá#)²‹î6m`zL†/°°Ã¹N{§ö;MÙî_1J&eÓ25&ú4òi`9n Í‹ðØFü¬‚á}D¿Ö„.âõVÈ4pÖüÈúÁ‚ô颿ñC(}d6[iÛ{I`WQñÛ>1Hˆ8—2Ôƒ„pÄXÇH<œ—~ä§ž³ŠïÁ%äÛCŒàXäÛ2"Þvfp ³?ûÅ×”o ÓPȃuä876/6ÂgD`að³?çÝ„±ÓdMd׎ò›,zïJ¬#­ï{üc@Ø^å‡Ã-mOÙMr"åÕwt} -›ùï·}âp+LCÏÆ¶ù«lßLÍ®åGÞæÓԵȳ¬U–ëTK {2ØŸcΘüÞ½÷'ÎBWò y¸¡‡u€•®†á†À6¬Uµ ÛƒŸuˆaìT^iQeâ_Z?¿ëà^=ßÄÍcV*˜u"'XqÖ-lÅRßÑõ%̶E"WÙ¾Û߆av^]ŸšÆSfã ™Í,‘õ]_‚Q½U¶ˆp&D68cG€.wó‰Ê*ÛgÔðªI«ak_®BéeÀJæAÄǃÇaŠ9T{Ð8øa‘¢ ¿Ç'9ó˜ðCcl°¡‚u £ 7ÖAØ1(:żILC`›Ô' Ùm„ÁÏ:tÖåÀ!Ž‚—÷ë@ßò–ÇI+û{ßÎæ²´:šM„G–{ûÔwt} †œûm²$²ågLOÉ®²}F ¯šäG¾„p)Þ‹üû7ÉAôÅK]½uûýÜÊYŸ8 ÛL´áÔA89‹šD"maúøÇâ\2ó°É‚¬ò‹9}ªS²qÁ·‘ ò¤‘“á[EÜÈèφB¾;ëà-FFxx•“AQ.ái»ü5v—úÇM"°0øY,‚Y{†Xóƒ§æÚ¬c¶Cë0Ã2Ï¢ëV0Á(–Ùr$²¾£ëK0ªç·}¶éЩ7ØW°[;ɼñ Î&yø?¤ïpø2)NJÒÅFµäõD2‘’ÒôMŸ072Jn(仳mfNÒ4rã@C`7®yÜ®9ÛƒŸu CdÖ>?³öh­ Œu”ºUIE)ƒÆÃ@ì·˜úŽ®/Ak; ømŸ˜$™^k!'؃—zlß´>Ï'Æü,µ0"=Èëûxî}q›4)÷¢.cƒdð†RÎûqOn‘J6™|C!?ëÈIšTפ†Àv­g¾ÛƒŸuˆ_‡X–ül(·-Yn®¬W•72^šÆüÙ›˜yt¯vtò‹Óå¥×§_áÆ/ù—œùë;º¾mæ4à·}2±’ËaˆGIBVá?­ØåcüÈ—.ůC>ÛyÌaNä¡:±h@ ´™|C!?ë['ž²5¶S £ØÍØFü¬#MÓCè¼’¤€¬¤»{(i6â7c ¹`ÝFöŽKè‡AÐ;"°ãÔ!ÃP¥/õ¬ÂÿŒpeýÈ{ñròwåü¦„0¬jÚˆQP=)MBXŠb¡nor®uÖ¡Pèª=òEøøl# ~Ö¡ª /A.KÖ\snÃ:¨F"BÃØ  ÉÝu/ÞZŸQ`•îMƒ÷>Ÿ¸|øm"±Jºê;º¾£Â~Û'½À<‹€O@&Y´Sjñ7jyÅ$?ò#`/WI¾¬îäŒo>µ`XE‰[¥å“5Ä …ü(¬#ûŒujø§+” (Ú&5¶mÅ¢´íØFV±˜b`ðœƒùzÌ ú ƨÂ,Ù†uP‡ää6š#*7¾»f… ¼J÷Â7^–¶NO“úŽ®/Á¨¶ßöÉÈ«o¢‚åï®ÃߨâE“üÈB>›´yæV˜dáHKh—þ¦™¡†÷54žŸ$§)(}ù¯¡„u L€”†Ó|Yû_ný‡¤s䕇·žÇ#ýÜóõ–½ùô6 ý´à¸:ÛƒŸuÈž“2¤ gˆŒËYgÎÍXª•›8p5›‡g/‘«tošÑ¾{`ÆDÜÞЈª€SÙÂ_.mn0ŒŠ¤ æ[L}G×—0m‚ÆømÌS–-ßFŸ†ù|aѨ#Vá¯õy>?ò#`/W!ÏdezÞéÍÁK‡K£ VeÆhâ.£$ú„GÌ(¹¡„uÜPåué[£ùd§²üL$Ó—Ó_‰ß»Ä3н™æÑ˜†Àj™8)ÛƒŸu ºé1¹•ÜÎÿð/Äiy±%uªë¶d¨;(ŠKžJÂФi•ò˜Uº—¶ã×!²D“™; L$v*`Ü$*ÀÇ:A¦÷SßUTVÙ>ÐÀˆÐpÁ_fârÌóð*ü§ _>fò9°‹a?òÌz ½<I˜:=ÅUf áa”’9ópq™èzᯡu$ŸóA¥0nL6ÁØ MOÑ2xË+£F‰¥Ü¿’À˜u ]Cɼìl¿ŠQÿH: Ÿ2£½~Ö$sè`BzŸbPq2?Õu›±ÌºKꓟa ÓZiŒ_÷ Oº¾åaRQ¿)²À:d®<ÜKêctA}G×—`TÏoûÀÀsüéHˆ> ¬ÂߨáU“üÈ€]¼ô#/.¸z ÈÉçóÅK"g1Zf ÄòÊ.aØßÒ_C!?ëøúí·4œç…ñR9ù)µxáf’%Ì%aoÎ:ÐKÄ Ÿ! ï_¸‘QrC`»DÒ)ØFü¬C,)ö¾Z“qQÄþ6¦:·ñõ6¬ƒ È“ÅÃÅ õ¼­µHÑìÔ¨e¿îE`Üà.hT7•%͹¼xÉKÊô_SÞDPÑhQ:H2ÍIL}G×—0[1‰ôÛ>Y±&·cý: lí$?ò‹4c”Á/ùðzSù6"\b1gk¾*³˜]æ(Ç,&Oîl±ÙPÈÀ:d\}%ÃiÞÖüø(щ…ôaôLÞBÄ sôH%ë0+’2>eY©ã ŸuÈz )CÏ™0‘БfãrÖ ¥#¡IÄ MšVϯ{Ar 3ùÁnÓ‰1Ö…ëúŽ®/¡Pµí·}Ú Ó+ââ+g x$?òSÙ¶cÖJ>9–Žª°¹2àϜޗ6·!€&1Ðh(ä‡`¿ü¬ª8} ˆüò«1ØÁzÆFùh’(#OC`»DÒ)ØFü¬;"feæ|÷ 3/¹ºÛ†u’éŒüî:ÔÄêáÍb ÿÖHT}G×—`ÔßoûD0x/¦G„…J×4ÁߨáU“üÈçbï ¯b´q½gÂöšñU™¡49’_‡ù²ßPÈÀ:h;Ì&Ë7n j†$3¦*^xšÝ y9[n„’ß7ÂgD áSf4ßÏ:IÀòγ”í·%ë@‘Nõ-2¦$§IĬҽ†•Iõ]_‚Ñ¿í^*Lƒ0“qÂC‚uðI~ägÅÛˆì.ùïß7YÖóã¤ìº¡uÐ2!] qp@º†/Þ¿á­GZzý1ÃŽÍ™™›uWO‰À6Âàg(1|9t‘Ž ˜C³‘´ ëw†GÖí,V¯¾£ëK0ž¿íƒ—ê8ÜÓÆåt Je¦»í3Zu†$?ò ©3° yž#|?`8>¡8”V»ÜJ{ÿ†<âô¸˜¹¡„uÀh»Œrp¶wÞO]ࣳFH¥Ëð™á `gn¬_H"ç1ØFü¬C½FÕ²°‰Ç6¬ ¤JêÃϧ¸z/Cý³Úx•îí'$õ]_‚ѺU¶â‘Üiî_qf§zÆÛg‘—ȃào´}ߤUÈ8O“œÈ‹WO1aœí½¼ i ?«a ~h`Hjšbܥؙ ùAX‡¸gÈ tØn-@ >ÀË!ÊÍVôaêÙ $¼I °")C áS–•:úYÅM™ ®›è ‘í©NÓ˜mX·ã…úö é Ü=Œ@´nœºw YëëúŽ®/ÁhÓl5%Þâo´}ߤ' ŸÃk„’Ÿ+¡b¿ÆKw ݃‚áG ¹j¼x™v˜û“Ìâ.B:³´Ff24ò#°ùð.Þ‚•¬è)-I¦ù¢o!'’CˆI$Ü`é 1‹;4v®Ÿ#îLl# ~Ö!feÂè p&Ò0ë›±,ï 7bÿøªÅ³Yr8…ìÔ½½…¦¾£ëK0Ú¸»í3êví¤Ý‘Çñ€ƒÇ„a+âWÀ#_òÙÐåŒt¡è2—,©dÖÔÑå´g ùX‡´—(XX Ë—ÁŽ` …–<:>|¸ýv”ãñRRu:†hƬgþ7v¦ôˆ:ÛÃZÖÁ,¡°&\dôƒz–Þª¶dÔFÍààêDïÚõ]_‚ñdìnûŒº];é Ècyj| 0Ù?l"©Ìˆ%µ3S™™•¢P,FÉäi(äG`©ùwL—0%/n4U"…“°Ñ±døÙ/¾æ·¥ÌR2Ó7Î阆À–ªñgA`að³Ž4_?,Æç¬¾‚‰r¿ ë@_1ÑÉ»@é(Õ0Æ:<ÏÂAlŸ§ªËsäñÕÑy1F œÓã0¹ÉC÷@i™%§ÂüÖÈÜPuÐRY,LÃÁ j4_æ_x…½r–’Å[Æ3£ÅàGÒ3A`að³Ž4ñÊÄî RîÀdF#•l:ñÛ°tˆÔªt.Õ0X‡çQòØ>F–ðà…þqf À‘^¨ „Ž¿„]òx/akǯ•|Ìœüa±h)<|(ö›ý#'»b2Àû8®¶q”ý3%a™>È û$ØP„uÐ<œ.¤á j7Ÿ 6˜Ÿ€•]P,‘EŒƒ ×gC`að³ôÆšæ‘×Þ€lå¶ ë@9':T>J•\«{=Äfƒ˜Ú<'££ uæ1£p}G×—0ªR~é±}âT/¬y7RŠW¶%þyÍÏö _ÂÖŽ¯—ü’k´óãjÙ÷oÒeù¹Ùò´œåcJC!? ëÈ>E–HëƒÌ?Ü3dÒÄöÓ¸•Aáò‘¾aãS£à†Àw‰¤S °0¬b¶*›¦nÃ:¸/tH9ä¬1Û{“¢Ï9r+égÍSßÑõ%he¦EÛ‡ÝF™‡ÂøI€Üz…„Leƒ˜zÛ7­í•b‘ŸEÕY|Iª‘fa–= Þ½f· ¦`ÒÃò¸%¸Ýw …ü ¬N._†…rÈÇï x—IO™¼[½xɸ¢‘²Gá¸vð+Ü¢”¿!°¥[DüYØF.À:h¦GLÎy€0GI×ëÞ©,A´xÆEëÊ™•†¨‹iN©ïèú´2ÓÀ¢íÃvÐ@fð¡|²¬‰Ij0g(,éÉò*§øOëÞ˜EäK‚½_¼Á:`›âT`,ðDfÒ·WñÐ-Á푆B~ÖAóEAÉ™KCVÑ* 23]ÂA€Ë·æ¿üËE¥¾Ý=ð-¤0Kn¬QÿH:ÛÃXGz­ÔWé\RÂõºw*H°ÞGÔ•K½Ÿú®¢²hûà4†ƒ3Öñ {muü§=rÞ˜EäK‚½_ü<ëìÝÍñcÀ§…Yüñgà€©²&wÂ7ã(“s¿i(äa´‡…†ƒ’Í muü À‰œÃéãºZLžJò¹´Ø™Ô†ÀÎÖ'"O„À6Âàg:y±¨Ó42oH»³/u/Mà±’3dz̻@~h}FzÝ;mM¦Ø<>-sëü®ÑUTñ—±ÎH5‘Ž?RÞȸ,íèÒÿù³‡‘ɳÿ²ùYÖ!‘É?áý†ôÙ2‚á>£¨†˜EòÈ®Y†ºh(äasÔ”à3‹§B'kXtËtY’\biåìð5[If/Y.·V+“"Ðð)3ð°,sµx r0Ÿˆrã±å‘!ÒVtÛ°ê€wkšñ¸c¨Ö®˜¤ÖëÞ)°Ô„#Gc0ԜnjÂõ]_¨Jùå¢íKàÈ3P ñ0ïYi•å_â«=ðÏk~öð"ò!ŸÍS|ÉJʬŠLç4z½OyG (¤ÅÈÜPÈÀ:@zGšÌ—am-Á¸š FÇAÀÈ ßK“\ßù®Œ¥¤^0;7Öè¾H:Ûƒ‡uˆö B#K›xlÆ:xx©/Ê=ð¦‚4«u%²^÷NI×N“J1õ]_B©nÄ{lŸÌ¤ ‹zÙs|ýÍÀÖNò oˆ·‘T/ù%ÖÁà?3,LÜf Ê›YÑv&VtÖ9ÑÿYX ùXG‘¸{Èe«°JŒí»\LZq€-7 j/¹Íë`”IÏm„a‘u(ÁÀEAì»ú$GÁDZC¹mÉ:¤pôñˆÇºÙ[FÔëÞ’&¿8¡^Z?XßÑõ%”B¼Çö åƒoÐ ¬Jêqp¶—˜’„ôÃßhΉ’<È—°µãë‘_Áœm{—i›Ý†B~Ö>€ƒG(«WÒgëYÔ³ôGfº•‘gOæ¤dÍìRÉ ]jA¤m„a‘uȬ½¼±Êk¬¬P@þ…„”fíɰ ë`LvÁ¨‹âß(Õcp¦¤ëuoI†¸5•Y^?8ü¾¾£ëK(5„øU¶úV„—Ò)œKÈK|?üæœ(iò6Ô£Ô§#ÿþn+¡Ã9¤ÒõyŒæÙL[OèÊY%ês¿i(äaù—î™4IôÃø{û9óVªÓ¶ì2¯vÉ 5ªI§@`aXdbÁá(.÷%&ãêÔs¤ÓôrÖ‘‡±tÖúŒO×½K”jâX?(ÅÔwt} Fƒü¶ÊýCÑiG Ky®~øÍ9Q’ù‘`/^®BžÉJý”˜á„ˆ?¸$~b¾h§gä;ßÕ•³u£w ùAXGb÷¯˜â`x‡Åh~ÒÃCfFHÈÌSöq4uò3J–ñF¦WKnì¤"q2¶†EւˆðDðËêo1(˜.%¼ûX@ñd1ÄPÌì%ŒAœªâUºw…ô¸×J™õ]_‚Ñ:¿íÃjÜøÆ0û,a$g|º£þFcN•äG~*ÛvŒyJ@$’kâ° B†ëÙJ”ËYÓ̦ô;ça~KËlf‰Ä9*‡ìî²ÍR‹#°Z ’ºÊX蜲»)hdþò«Û`ˆd.®aùæ+2ƒ-ÃÔ(g\–2s£®ÚcÚˆ92ÛÃ"ë@nZØ5æ#)–!|{¥½U²)è½mÆ:lK*Hò>ÍæÑ½Oq´K/z¾õƒõ]_‚ÑL¿í‘ÀúàÚÁ‘œ{É)ñÒNøm9W’ù©lÛ1äej5¸Kè }: #^̹P2K'$ ³A$pÒ=3eå¬áÚÑPÈÀ:h)àèÜ.ñ–a ,²y½<:†?t —%¬Dÿ»°áÓ’õF Õ2#pR¶†EÖúÒ·˜[x0%<#hC¿„u ½™ šÖÓ£{Ÿ&# Ÿ2£ù~ÖáNLãîA¦Wd˜ã{“¢½7ëYW`¡É¾Ð¨ ¦V.¨ïèúŒê-Ú>è(䓆Ϟ±µ¶Ï¨ä%“‘7x…´ ùä»8<øòÈÓÑÚÈ’»‚¼¤6¼IIeTP̨8%7ò}YGòÉñRXœžÓl”é0“«àµÚ^ÀÝI§@ áSf´×Ï:’æyñR}9ðô©XêYRqÇëØŒuˆ;ÄŒC6…æ•%<º£¾£ëKU)¿\´}dÙ(Kâ±ÊöåUz&áEäKÀ.ƯBéeÌJ†÷e]Rñ];ûÆ:ã]öwTéDF˜¹œ¹4z¶¡ïË:øä ó#éyqïê#NbrF¥$¬Êã¨{kt_$†O™ÑÞµ¬C_]y—Áoñ¦ž%-÷ Y‡@VG³I˜©pP2Þøê;º¾CBm"”Þ‚ï5Zš½%ñXeûŒ^5iù°‹ñ^äß¿-Ó¡¯_ÿT@–U™¥e˜’JNÆEe†‘ß)ʇòy2ϧÞ)³¡ïË:T\Gý^ÀF¡IÓ@¾©¨6HŸf“˜½€-Õ'âÏ‚@çÌh²ŸuÈ žøƒ„¼ƾì>õÕð0¡†ŒÌLU±W÷ð’X?xóëøð¸ª1E×wt} FõmŸÀ Â2Ç{1áü˜"/1ýð7šs¢¤EäKÀ.Æ{gÊCöøbH“‡KÈÓ+siàN( ó‰È?·o¬—( Ák‹ú¦*c)uPC!?ëö¦‰†,8Þ½F‰•ÚN<°'õ!gêsqÐ^Àõ¤S Ðð)3ÚëgÌ­Èû,c§¡:á2«è¶ë@‡P™Ù9”ÙZi¤G÷ÐI¼Âèº40³-FæúŽ®/Á¨žßö /Uxýð7šs¢$?ò‹P2xç'õÞPÈÂ:@IÜÀÀ6kîå¥93ïì³zذô…%Ƀç-(1ÄÁT ¸ä×ÁCG±¼ï9Ïrïý‚Œ^‹¤s!ÐÕ”(‹¬Cõ•¬P€„hÌb`ÖÁ› O™x“ŠÏ‰ž%PªçjÝ«¨™õÆÇ”ù)qì'²ô£úŽ®/¡T7âý¶Ž–ËšP}ºÔzÚ ð7Úr®$?òSlí˜UÈ‹!£sÙT“þµ—Zø3ÃIl( Ï).Q)ü5ò#ŒuÈ7vå»6¸ÐÈeIK0%X 6ò+~2…JÜ?xßá`. 2Eá1œæÔ˜†Àj™8)ÛƒŸuˆ5±ß^GºnKÖ)Ý}ñr•îõK:¬Ð 7ÖñÍW\3×õ]_‚Ñ:¿í£½"$·ó0»ÄKY‰ûuÂßh˹’üÈ/Šú(Ã*äeS¸ô¬±ÖŸYÉ9örN×PxÉì’ÚPÈÀ:¤ṵ̀Ðj† çÆbXaºA‡üÊÀŠþ¥"í²¹GWýs®Ç*jk Ðð)3îâg¢OdàS"s‘F -¿Ü†u œžEwå·ö„Wé^Ãi•°Âà2Ý`è~[ßÑõ%L› 1~Û'&‰ÅÔˆ ³K2~ŽM™íŽ~økåOð#? ¯¹ y†7y_æ%:M€×gUfüH6¬r.;¢30Xúk(äG`4µ€ŠÜZðÞTj{Ê<¬w¦¢x²ŒÌ@Šî• éK÷å-VÈÓX£J‘t ¶?ë`Í,Ò;= å¶ ë@‡ðÌ»„•j¸J÷®’œóÑÏht5 Ulü¼¾£ëK0ªç·}" -¬ƒ€ÌÊQ½Ù.臿ќ%ù‘Ÿ…׈\‹¼’ 0j ”2‹3bCU1”<)¥œÄ7òƒ°œ4pú©Cp0šEá`ÓQò Xì•D¨]äÂ3¸°Fý#é4|ÊŒöúYš×XÅè0”Ûf¬Cìà L+õ%­Õ½Œ3IøÔ½{½ìz×Bv¿í“Ñ0qí@סۥ_b†eF<Q~äG‚½xÙWòM“,Ь'üG´ŠÊÙ ùAXGÞLF~@ …ÉÀ!DÎÞÿgôCÈ-ÿû7³«ø;ºu\žm„aë`ò‘Á=™[aTpqxaÖP2’/6ŽŠ9O:êÞacF`8Ò’@󯾣ëK0*è·}i÷ÚÇÑ0Xo¯\2DV2‚ñ7Úsž$?ò%„KñGA>û ;~¤öwT ùAX¯HßJ–¾t8öR”E¹VGšQΆÀŽJŽËÓ!°0øYGòwz´)°‰Ç6¬ÕÊÛ4o ir¨!¶^Ð&¬¦]PßÑõ%Lk¥1«l¯c`«"¡Ù.臿VþÔUÈÏ"\Š<ò<2p*g.þj(äÇa¼›ˆSGr+4¾!ß ØZ'Yù¬c%`Ï1{ç̀ÏÏ:yìˆú Љ/­P@ãmÆ:˜Rá­‡Q*ɳ )âq–óöº— P%ŠA< zÓ¾¨ïèú¦µÒ¿íC=ÊàFD„·Ç_+ê€ùÂ¥øƒ°Ž½>È~Öñþ AßMÔ’ƒ(^2xˆë áÅgÅ>XÇ,,™#ÐÕ”èÖ²& „u@?°,<8»³šó LüâgéD÷Ó½ nÈûc§@„å•TÖ*ò¨ïèú´2Ó€ßöÍ4ÑüƒÛ¾i“ãG¾„p)¾Ÿä¯‚¢.”%›¨6ò#°i¯|éž &¶÷AWhc§ºŒ—8‰—-ÅŒ-8¦?'¦„pC`gï‘'B`að³™RûŽUå)À ðRoŒ¢o3ÖI5aøñ7ÿÜÉ7z³©ŒŒê¹ô.SßÑõ%Ï…ßö‰H '£µN·}FÛ÷Mò#_B¸Ö!FP¿t/›€7ò#°Z*³*2»$zÕh>/S²l–<âbgdžM Ö1 KDæ4|ÊòbGa?ëH›>=ºkòV+&ûRÒlÄoÆ:x~WmÕ.uî§{yƒ[ܬ@;¢¾£ëKÐÊL~Û'c_ ‰J?ü§­8cŒù°‹—G@ž„w–\tíG&ÏYÙ›a¬„e†…—Ž4ibnüÅÄ ùå¢cDWÁ,¬c ¡Ho¹+ަŸu Ê0(H;d+¿hè·aT ' \I9Ï%%ÜU÷>Ã/Ýˈïn¸²äÇ.ø2–¤C°ŽÇO¹}mó1m](·¤‹¿¼ëHMöÐHÛh,}¼>ÇG7î`j†Ç-OúÎJ–H¶Fçíìc†ÇPC`‹ŒÿgE`að³†õX'‹ï´œ5P²)ÄoÃ:xsa|Ò8J5ìÇ:˜®Ìû Y9[ÄúŽ®/¡T7âý¶Oæàn æ¼$¼=þFsN”äG¾„p)Þ)ùòÈ'Ä`r¼ý¼dæXú:^þþ½Ǫ¾h(äë`,`Ê3‚úšÝˆ¨4|Q*YGKòb›á3"°0øYÇZ_A4Þf¬cjéò˜JÝûáA‡0"$ŒQc ´¾£ëK0Úè·}ÈR‚ýþ•®ÂÛão4çDI~äK—⬃ç—Å2ê`'ažû£LŽ<¥hóL#'¼¶Ï¿h(äai/‚ûWL²°2…Q°µÈa“p áU%7vZÈ9ÛƒŸu||“åý}ø(€œKšø-Y¤Hœ…‰Å—ù R ºw­Øˆ*ൂ•¤,ðIó×?¦ÙUTü¶Oü:ü½ð_Û_‡ÍïG¾$á¥x'ò<¿é9úë7ÂoîÖAfþã6Vo?—ïŽð2^Š©%‹2 6¬ó227òƒ°Ô—_ݶ ”×óE¦Ä:¦%“s¶"[ºEÄŸm„ÁÏ:P)3–]ÌŠ8 Ú®ƒ[²è½èØÄó_¼Dc—T®Æ;uïZ‘OKë—îy‰£>Æ2·úŽ®/Áh£ßö ßãCgßd2N:áo´å\I~äGÀ.^:‘'/à<ûi–ðý›dæ N˜rä\¶±Ò31æ"0Ž!gïÇDk²/ë@3ð€PÔ]}„¡Ùþ´Z ŒXÇlÉi¶÷îÁ(¹«öЪFàl# ~Ö!jçf“ Uz;²‹V£pêÞ'H øóPX^…ŒBê;º¾£z~Û'FDä$?o¿Ñœ%ù‘/!\ŠwJ>Ï/…¤QÍ»ÞÇ1^¥o´‘±ÇA” r0w€€3ùÈÌÙdÙ,#“Fæ†B¾/ëHx¾xùÍÏÂû*ÊA€K£ù³I#Öñ´’;[Ɉ<Ûà Ö18Fò¼è$/†•pI³¿ ë  bã¡•͸¸$Ò¨˜$9uïdÝ‹:EQ£QQÅhW£úŽ®/Á¨žßöÝÖVg³oö\?üæœ(Éü¢¨28‘gƒ¬!»W1|7눘ƒIfŠÍcŒͦïàФQ ¡ïË:Òd ºôÓyêô¤˜ m„†\ŽXÇÓJnìl%#òDl# ~ÖÁ"q1î£óH¡å—Û°Ž4<ûȈnu»{È«Q ;uïSdæý´¨(êt6¿ÖTßÑõ%m\kûè¸gYË\ŸøŽøí9OÒZä ¨GINä¡‘8ß=þ0mŽüÏɳ9à …|_Ö!šµ@ÔN(4[šÌÍ&¼žVrC`µb8)ÛƒŸu¤™‚/ysM9a—04ª ÕÊ›5g.GjvöÒ©{Ÿ ?¿÷ë/˜c•âÑÁ £QH}G×—`To•íéƒá/f«9ÓjbfÁ'²þFsN”´ ùȳñNäéÇñÒ×÷o컓ÇÂã²é²Ñ_C!ß—uñ0ßMÆ¿zÌ\šðJù‡-;RòÕÚò_C`Ë7‰”s °0øYb°Ѭ›Üf¬CnÍ˵³•™F:uïÄ °¶èg™ô1 ©ïèúŒêùmM–±&€•€œé—)øÄôÃßhΉ’üÈÏÂkD:‘çù•‘dÎÒņ¹ä/f6Ê™&5òƒ°ŽÄs%ö»ÉªÌ=ÜSq–p³ÅAÓ^‹˜s!Ðð)3îgâ+ˆ/Ê$? å¶ ëàáâ±Ê«4 —jèÔ½z¥$KyÞš–jÂ¥œÄ×wt} Fõü¶µIK“eDlò,Ò÷ûáo4çDI~äK^Šw"ÏóË~æaSäÙfRå'É_Õ …|Ö1 °b_5Ù¨„@ØU™Átøš-ã½<œÅ§W‡^§˜7vZxÄœ m„ÁÏ:˜[I*er”4ñÛ°tÈ´VyL©†NÝû±ö ¢™fåoäñ5-­¾£ëK˜ÖJcü¶O0mÔ•@båïöÃ_+ê€ù’„—âÈã×ÍbÏLzVVXàŠ(¢úé×Û‘¶ø(f^™ÐPÈ÷e(YPO@œó ò’U™åç¢pXDtð™ä]Vøklá}¶?ë`œ‡Rtô#?—4ñ›²Žá¥Ld8K ÕÎVÒ©{×JŒèý˜¦8–…Ôwt} Fõü¶OÀ—%´¨°V³àÙ £-çJò#_B¸ïDžQD<”xˆxš`„m¿h^¨ÉÌ\@z…÷šI™V˜7ò}Yc€â  %Ó#}ýmŽu¬Ê,PË~AôÂá-hl£kÜ%’NÀ6Âàgh0œ:įuÄÔ!‡íæ±%ëë–Æ„_¼dß!j M"ÌQ©{×J î£Ü4ï>ËãëB3,kGܶo-þ—É¿;ë§ Ã¤i þþd¯raì)Âo¿¿?7’òÊÈæIÚ—uÌÖpqD4ÿÕbfa‰iª g¼»¦ÅóŸÂ •—§C`að³†ì|¥NJ¨ãM[¿=ëÀQ+a¸æØ˜u i¼ÂŒVâWßÑõ%ÕóÛ>h^>–Tß°»ÔöøÍ9Q’ùÂ¥x'ßc Žü5™QúÒ+¹¢Jø‰¼Â§ÌÙºNÍó´@C!? ëHæ^‚9h‹™™Xùd¯6³›W2ÂgD`að³F2âwØt ˆM/ù ¢ñ¶dâHpÖ±JÞê;º¾£Â5¶"ûçÏš?§í3êví¤äg×H'ò0ç©?¤Â{i”99ü峡uÐÀ)Ôc|Þ½fÆ$}šöÝkñgx¼Nÿýöö…âJ½ ÙûxÿøV¶?ëŽS´LߣÁdDý ¬CF]‚u4÷Ým_ó¥À3"Ïü c¡‹ó>ÂdMZ jn%‘—ÓP„u°¸¿Yå*k“u!mÞp ã3ü•(a ã·Ó<ƒ/§ËÇ•¹š4 4vZxÄœ m„a-ë`žÉÇÄóË„ £‚ú5 l3ÖAQv-cf™‘a·–ò>¥Î7¾ÞbSßÑõ%m<£í3šs¢¤s!/¯áhLtBŽ©@3$z÷€Åà¦÷÷%ûØPÈÂ:h»¸Á ©$`Œ`$·ÞûW2oBf~;‹†$°å,Þ5ÀkS”†ÀžèኪÎ"°0øY‡ qˆH#Õ‰rrbÞ?þ}G×—0+y.Ûg4ätIçB>}1A>L _‘e/s¸3dÊ ‚8Š$ƒ;äLC忆B~Ö¡¡èU8P NÓŸٿO—$§eq/^RÂ4¯8Õð Æw ›çÜ(Ê4ëcLC`‹ŒÿgE`að³õé1kî%r›±£vRŒuxžsÙ>O‹Î’ç\ÈóžÎÁ3…¢ €åÅuÁ’®ÀÙÐÁ÷oød(–™œQ õáX‡¬oeÞ79ô$­"›{ûO†/RêÝãÌËØ—ýæCêÚ(ÑBoæ{aÈÑØIE"âdl# ~Öçõ$ÿ añá,÷`™«ïèúŒzžËö 9]Ò‘W Úø‹bIgg °‰iî»×˜T&XàÉ+9ïæF5ò#°`ÚC`™yY¬P­Ä3ÅÔÙ€KæP¸4°Ê{AJ˜í)¡!°F•"él# «XG‰]”âƒux$­¾£ëK0êyFÛg4çDIgDžfOdÈ~Ö±« {Œ5±é(«k ¤¡¡òߪ’»jr#åˆl# Á:vïûúŽ®/ÁawÛgÔíÚI»#ÏB u ¨"È ‹4qBÀƒ‘¯à*°ŽÙm%4óèƒìÓ¥£ÚÅ …ü¬cV€@_& ^dM áÒækKn¬vVNŠÀ6¬cwñ¨ïèú v·}FÝ®´;ò YŒ¾0Ë»¶A$ò=(`˜E2—vŠÌ2À‹ü–d?ëažVȹlÂÆR"~ÜW?kKîª=òŠEøøl# Á:v—„úŽ®/ÁawÛgÔíÚI»#Ï›5¦09‘>úyâzÁÚvˆŠ|™ší‚a,væ½>È~Ö†ŠÃ‹X1‚ÄbXa¸4þV•ÜU{•Œ¤"°0ëØ½ëë;º¾„ÝmŸQ·k'íŽ<Ó+æAjYîjÀ.¾â ‚íMÐŒ~ˆ Å€Ê÷¿eÈ/ ùAXG«GŽ—7U¸Xò\) +>â΄À6¬cw™¨ïèú v·}FÝ®tä1‚|–/§¿SÕÏ;â퇯ӛøû7öûxþ C?Œ¿†B~ÖAcÁSPR¬˜–bÃRgÒ´äÒ[ºEÄŸm„!XÇîòPßÑõ% !ì$Ùã`¿4éoÔçù$uàqÁâYÖDp` Y몳-¥ŽÀUƒI{-?§4u‰Ä±•Ê$¾¡‡uLÛ+¾ Óø~1 íWÉ(y¶îÒàh™õf康k×wt} ]X*œj׋G©ð Äï.ùò!ùB ûu'7Œ/Gþ¥#œeó ~’vù~ñR¾±Qv‘ç¿ex÷šÔôE3s—ï†B~yÖ…ûðösá‡8Û{»56ïÜŸ C³²»îí-<õ]_Bï6ΖßDã"ãT9ì£ðIµÇ¨qÙ C³²»îmÒ›F!õ]_‚Q½~IMÄ£_õv/ywɇ LÙæ ËX‡T,¤2à ñ L$ i§?‘]ÈdE†ì2Í£1 …üòcÀæ,¶ÕÍ=Æi !°ÓÂ#æ\\@š˜•Ýuoo±©ïèúz·q¶ü&â1[ò5"w—|ÌVòlÄ•ôñO Ì™1/ÆfV¸g0ªSoÙØwÙ‘RñXÆãÿaoROä:X‡àÐd¬"ÇŒ+§"Àú GÐgþŸT{Ì´$¢ª¸€041+»ëÞêž\( ¾£ëKX¨bŸä&âѧj‡(uwÉO‡ß^ïÐôM–/ù0½Ó%äIgv²z<Ø#}úÙÌêßýåïJ’ØÇi6i(ä—ëPÐ<†ÀznyŽŒÀ„¡‰YÙ]÷ö’úŽ®/¡wgËo"³%_#rÉÿ†·æäDzÿŠCÜJmleCö‹à‡zžý Ž”l¿†ç?l(äGf‰Œ™û–ä˜4 7¶I}¢¸€041+ûëÞÎBPßÑõ%tnâ|ñMÄc¾èKÄî.ù,šfÅ ¾£0þ˜X!œ¶*-ÿ‘“¹&b8WÎRŒ?sC!?(ë`ÕÉðaËÿXÝÃ,UCØî…Qfû²!°ö"õø\@š˜•ÝuooQ©ïèúz·q¶ü&â1[ò5"w—|ÜAñÍ`bo <äÌŠ/3&dH‡ãK÷·Ì2óâ¥Î¶Ì–ßPÈÂ:—x܆w žm8½òdÖC–'Ïf~BdC`Ÿp÷øÉ¡¸€041+»ëÞÞRQßÑõ%ônãlùMÄc¶äkDî.ù²K9> Ó%)¦Ì:èÀtÊ—îqÅ$e~4¬£NÁ‘ƒÌ8¬à Ìn!3¿m(äû³Ž¡™€#_®§ÕòY½DrImÉ™&¹„ËÝlæ'D6ö wŸ C³²»îí-õ]_Bï6ΖßDÅ>Ü"̓”ÿØŸÊÿ¥{yÙ—]Ó)V¶+•ÝPÈmÖ}§±ð«üœ_–P%Þ ì´:L4M¢`qp9¸³QmÿÛ¶bQÚö\@š˜•EÝk(;éÉ*¢­0Ôwt} m[ä,­‰x8ïuÆl‹’c÷¯°×·õ­/^²¡(a‰)É¿Sò¿÷ý¦÷ëác÷r曀,i™…”^äù ÔÁþÒ½”)yÖnŒÊo(ä6ëx2—«aiÁìý«Q“å’æIb”Ø~šý¡3²!°Î;F¶Ã"pahbVuoIµ.Æ;uoo ©ïèúz·q¶ü&â1[ò5"%_ì# DÔ 0zÀ¯ZI>‚ØR)áÌû8Ì$½•—¼CçâÏ»p£†B¾ŠuÀâv,‘³ïj•òî5k‡Óãw¯K\ŽoðqŒ†8¦kio}A9|úmô÷¸&w-— -?"O„À„¡‰YYÔ½†°“V«ˆ>ÒSßÑõ%ôiÙB©MÄcágN^”üYÖA¤-ö¤:%§ñuäüÈ9 ©ÌD(yàÝ|öãõØÐ)Ì,ïlf-¹¡Û¬Ø«‘é$ÎL1ÁÁl žœ|•ÆÆÖ ¬4 z°r#&˜d¦IÛ«fXÈ&Ëfeïcå,™qõLrÊÁ„‘=)ÖX­sNŠÀ„¡‰YYÔ½¶0RW©ˆ~RTßÑõ%ôkQrñ0Ê?{Ò¢ä'û8øZ`†80.\6d< ¼æçòCiK’õÞ¸a\N3óHŠc$–㶃ú\fýyC!·YÕcÙ/sI,æMƒ Dª 5j¥R(ŠòÅ[œ)vhˆÚ¦:hMURdò¬#¥ŽŽ­€Íká3"Ðð)Û«ùMÌÊ¢î5”€¬c/Áû6}›Ðõî‹’Ï«·° –8/¾óP8%FÎß~‘%†3Œb–H(b=aLLpÌff²@L­¸ HXöÑr¦†úp‘u¨ÞL8H[dMœÀ¦Ö ¿£ÕÒRY ;ë¹wìj¨ Õ=ù>Î"b¤Ëdz‹œ·ÌÁ:fÁŠÈ  Ÿ²IÙE41+‹ºwúì;cV¨ˆž€Õwt} =ÛW,»‰xK?‚GòÓûøðæ+¯·\z„ß)ùé-{ø”,BY"¡`§Áÿ»ˆöÎÞVB6¬È—ʲS––3 4òEÖÁ¬ aV…Ù Y’#|#|ÿÊ@Ø,ãEiò‡”Ƥ¥‰Îñù•¹Ù+nGþ43t´xt$›¹?Xî=yÊ"Qll~ߟ C³âѽ†0’<*bÉ©ïèú6hæôMÄcZìebœ’ÏË8Ë8Òž'9%ëÌ: œ Gp¶÷&y¬3Õ.°YkêÉŒe™!2ú®¡Û¬ƒ¯ÏäcGÐ @`Â…µ6‹{€¥#h8®Ðn£à 0b D‚ü¬ñÈɶ0ä€:÷'”˜ †ãÛ/`MÔ.ï-®!°Æ]"é\@š˜§îÍ•ª3ìQˆJ}G×—°A3§·h"Ób/³(ù³;Kä»L”§äcï°Y U†&0%„eÓ }ÇJâRÊL|²¡âðè´`dn(ä6ë +¯À0ôz”P%Þ,Û|}lu¶4†;Î6?±ŽûWºÂîws™Íýቢ 1T8ѧûW¥) !°…Eôi¸€041+‹º×Pv’GEl .õ]_Â͜ޢ‰xL‹½LÌ¢äì£J ”äß)ù¼ÝË–LÜ8Q%¿ú*x~3‘ˈ15~ÒPÈW±S%T‰÷›;]äÍ/Ñ9bÊ‘áW¥Ì’.Ð]OÉlÿ5Ö¾Q¤ C³²¨{ %`'yTÄrRßÑõ%lÐÌé-šˆÇ´ØËÄ,Jþu μÒÞÌböú\’§ä3j!^ )Î ¿±®Å2_ÀÞ~øZc¦†Bn³Ž?ùÓ?†ÿ0+eŸ=—P%Þ ì´ubØYw½cC`µÌœ C³²¨{ %`'DEÔwt} »<#MÄc—šosÓEÉÖÁø?r®ëYÈ29Ï>NÉglŸ!zÙ]ÎxWÂmZ5|“ê0£AáFÉ …Üf³ˆ9#ÀÍl’´°M*…ìˆ@çl¯V41+‹º×©¦Ù¢"ê;º¾„]$¤‰xìRómnº(ù9ë@¼ÅvdC§ä‹G¯ü ¤ðúO …[±Žaé(“8Îâ9³¯æ#Ö …Üf° øOéƒzÄ+ŒÓ€Xip9ùØ=ѰÒc»×ÿߨõu_ †OÙ^ kbVuïôÙwƬRý0¬ïèúúµÎ(¹‰xåŸ=iQòɱd¾ëàaâƒÍ9±Åà)þ¢M€•¢nŸ©}ÿãKýó…´£»4r›u—0gφzY¥Rè8Êç'Úwº­ë¨í«.wvU=#óhø”íÕº&feQ÷JÀNZ¥"úaXßÑõ%ôkQrñ0Ê?{Ò¢ä ëÀr±n…wp1‘¬\÷t"Kòï”|Æ:8’MvŠ`%lCÖ‘¾wöâ%/øœyë§Î›-ðt±Žá³w²9˜ìfFõáªÄ;É”½VXòœ–Î9ºP¥Ftw¶¦ÚñÛ# pRS’C×Ĭ,ê^C ØI«TDÞ®¶áúŽ®/¡m‹œ¥5ç½Î˜mQò™L;X:—äß)ùPY9+ûuàƒÃ÷jfš¯VÎÊŽèÜË(¹¡{X‡Œ ¥9¦/êIE»„*ñN`¥™¬.Xa›àŒ’övT¸< Ÿ²½ÚÞĬ,ê^C ØI«TD? ë;º¾„~­3Jn"FùgOòH~ú ú`¸9óîœöjÐ5,/^–äß)ùlŠ%¾̃&åsÉ-Z‹Á>Ã9ùu°9gù¯¡ûY@)ë`m‹°»ªÄ;-·²MÊ^À¶©}”² Ÿ²½ÑĬxt¯¡Œ¤ƒ¨ˆúŽ®/a i"»Ô|››:%n ‡ˆ:¯äâP !) ¿Wòå ,£ÖÎíÚ=Êò´K¸‡ñ×PÈ=¬ƒYÐ;ë/ó~ sRŸF|r•ö–§ïÿ6öñ†ñÿ¬4|Êö‚ ‰YqêÞ’‚5⽺·3|õ]_Bç&ÎßD<拾D¬SòÙU§–]Èæ™„ yœ’ÏüeÊž8`È*WbZ¡Ë|úO²zå _º‡“0¦!3,G`ôS<·oð dñÓ*È¿JÝä ˆ'Õ­„0Êɸ€041+NÝk°‹R’S÷æÒ#\ßÑõ%ôh×b™MÄcñ.çÍà‘|FÚõ:ÛÁ[Âpƒ’Øï”|¾v*¥±l6MÜÈTN;¿ Ç€~x÷î±hI ¹=ÖÁwXh)¾£Åxm·–»‡z`Wɤø–È×÷èëä3t÷l!9‘ÎÂBY“›@.ÿ5¶|“H9†&fÅ£{ =`$9uooq©ïèúz·q¶ü&â1[ò5"%ŸQ¥º¦nR*>³òï”|v)O~‰Ã¸ÞöÝÖÂNÅŒ2!«EŒ ¹Í:@Lg©rôˆ\üœX£™Ó$X5âC_à#{›LsC{¡C™8ð¥Y|VgÒØÙò#òD\@š˜•EÝ›k†Uá*â VßÑõ%<¡Úõ?i"õÕ8l ‹’ËkïÈ…C×xŠƒÇô¡ðJþ»× Ñ'wÿ z¶±.[!–v%½{Àž²|c‘Ï4r›up£mv sÂHgqHfº›Ç¥µ±¦ç-ùi£üDÒò…ßÒí[ºEÄŸ M0¤ÝÙ‹ºwªT1^Ýë¬èS³Õwt} O­{ÕGU ŽýãEÉ—m¦x oÙx3 ¼8S¦¸y´mØB?X)í8¬{-è•γ®v€²}+¢2 Å–)üÜþŒ,Ã\7¿Ž£!ÆOª=ŒEÒ“¸€041+‹º×Pv’S÷>¹?¬ïèúœUm›­‰x´­Ò¡J[”ü|sË‘¨ËÌ âåÒ)ùø3Pwl`ÈdW Þ²Õ´.yÇ&J! w@lŒ ¹=Öqc÷¯˜]ÒQ#rÉyÒUÀÍœ& ˜K‡r÷i†i ÃSºn…Ž£òÓ<ÓX-3'EàÂ@b¬cQüê;º¾„ÅJöÈÐD©¼!Ò 5îI§@àÂÐĬxtïèÃyéT½¥¥¾£ëKèÝÆÙò›ˆÇlÉ׈tJþmôÇÍ4XoÂbI¼4§À)ùØ;r&ŽrÈ—aw·¡Û¬#í×1lˆÁ¨æ^¸U–0êbs[ãLÇÉ^(Ö‘2Ün¥2LKI Ô L]Q[¤wĀųüÊØ.¬!°¥*EüY¸€0ЄëX”·úŽ®/a±’=24;H™NÖ!FP6EçÜð•\Œ£˜Z9‹Oã.ø4r›u€çèÞkÈÐÎõ¬Céž9?úñgài,á!3yX3+üO“ÎÐïS8ã]~%+_&ySDC`gËÈ!pahbVVé^C!L“œo|½e¦¾£ëKèÝÆÙò›ˆÇlÉ׈\”|”p} ¦2/1NÉÇØñ¾ÌÒ=n/Î{àÛPÈYä†Ë¨.µ¬]•¡Æˆ,¡J¼X°Mó b|绲G‰HP[¦c¬€ŸÀ ¶«Ÿê#;ÌßXÇ7_qi¼ý5v¹ˆ{¶DàÂ@ iw‚µ¨{ %`'9U„³žOÎVßÑõ%<¹ò5?l"58øo%_Æ"°)¥£$ÿNÉÇ2b¹Ò@Ø.løKïÑæþTý m(䋬CpcÔb ‡F–P%~°YôqeÊ|2 Ù` I§Wlï(ü“ü4!-hº5Wð-®!°Æ]"é\@š˜•EÝk(;É©"zKK}Gחл³å7Ù’¯¹(ùdH|c˜ôÇÐ`’ô,<¤$ÿNÉOßa^ÆY£ÚË05[ó@C!÷°Œµnl·Ø}|>KJ¼XÆO`Œr9€¬~¥shbÄC¦Wèß<Ã(L%ß»~ˆH0V3Ê_66/6ÂgDàÂÐĬ,ê^[©NÑ[xê;º¾„Þmœ-¿‰xÌ–|ÈEÉ—±]ã©¢ÎnÖ“—ñ¦ñþMi¥g'ð ù"ëE"‚áèl§Jùæç?á qL×Ò~‚äÛÏeÁ Ó+o?|]ÜQQ©·ŸãôKùìÆSÇè.Ÿ”9\4vZxÄœ C³²¨{UÙ® 8UDo±©ïèúz·q¶ü&â1[ò5"%Ÿ ØÄ~¬Ã€Q] ŸT{ŒZ—M¸€041+Ý›?øþ°SE4éM£úŽ®/Á¨^¿¤&âѯz»—ì”|^„Ó‚®çˆ_ât'L^Àí7èæ6òűpƒxä£Fy¸„ªŸu¬G–ÖòýËf§=b•+g-t"í# Ÿ²…njbVu¯¡ì$§îíY}Gחл³å7Ù’¯¹(ù¬aáœ×p^“õœFãEIIþ’Ÿ&†Ou †o½k× …Üf̪äcGŒ é!ñ%T‰w» qÑ:H“,¦ƒhrOÅdøcTª4w#»ª]‘ù€\@š˜•EÝk(;©‡Šx‚ Õwt} O¨výOšˆG}5[“%Ÿ°e ¤´³„Sò±wä}Ë~_º·G .×u@)uÞ„€8 Nå–n²A%é)ÂÄ,vÙIµÇ´ùSÀ„¡‰Yy²îµ)G'ñ„~¯ïèúžPíúŸ4új¶„É·w–p²¶•F}Ñ^|ËîfC!·Ç:dI†ÒÊ ¦«î_éPñ†bq» %å~ú+¶:§Vz©Yhö`¤jÇ-vYC`µ&8)†&f¥F÷ú!XÇîÏEñؽý*à‘ü‘û<“ ÉV¾xY?Öá|Ëî‚–ÜPÚ¬#W°`äœGá¬C¸ŸÎ˜HiûtÆ@˜_“E+:6R鬖“"Ðð)Û &fÅ£{ =`$õPO€º¾£ëKxBµëÒD<ê«qØ%?÷@`8B½„rà’Q~§äó–}ûÒ=»þÒҕš½al(ä~Ö!;†íË:d'R–&¥-;Þ~.”’Õ4NÀW5ÖY¥ÈvX. MÌÊ¢î-©ÖÅx§îí-!õ]_Bï6ΖßD MÄcß&t½»_òÙ¸;?ZI>`ñq³÷èqRÏë†úÐföC¼~•ÂDI¢4¬q–9'b`~¼^?èF¤ÅËs^³SI!H|9Ž©7.g³IdC`»DÒ)¸€041+~ÝkhƒÙ$¿Šè*0õ]_B×– o"¥Â/ï”|V.È[9VL ‡³2/‘NÉ—"0ˆú‰^œ)|l ¹Í:J#H:šT,Á’’á ^Ï6_i˯콿J™%]Ÿd`ø±Cöëq°!°ã¢ãúl\@š˜•EÝk(;É©"z N}Gחл³å7Ù’¯¹(ù0Öãò£–‹zZ{Òb¿X v–ËÅë6#öò–½ ¼ …ÜÏ:à€À܇Ð-A5‡zvª²A n¯Ð©4/G˜fµÈÌ@“|“Ezd63?”]Â^¿þ©ìMŠC)\4/pnì¨ä¸<†&feQ÷Ž|ÿ¥SEô–œúŽ®/¡wgËo"³%_#rQò`ì ç©Ì7Ù›Tü:ð%Ð5,˜0.w·¡/²Œ;4€Ù«4hO` cJS´5Æ©RRÇ ~¤‰ ‰×¶à^HÃO83ç,ýÁFÈM‚«È1ãœý¸!°Y©<%†&feQ÷ê#¿6àT½¥§¾£ëKèÝÆÙò›ˆÇlÉ׈\”|ÙÃj:“"/ΘËш> NÉÇÚ¯É[âÜPÈmÖzS²!Ücñ˳N`aqÐÎ:^Á¥†G¨Š_©8lp^Ø©cø1¥}²7ix“Ž0Ë Ÿ²ÂºG71+‹ºWuéÚ€SEô†©¾£ëKèÝÆÙò›ˆÇlÉ׈\”|Iá•V½q?¸MTïM*ãù#0YÂ9ŠÙ没۬ƒTßÀI£ÏA ÃG‹f…J¾V¯ëaGž#HqäÀ/”̬™Mø›_®—ß2#CfÆÀ™;ªv\ž C³²¨{UA)à ÑSzê;º¾„ží+–ÝD<Š¥Ÿ?Á#ù|'EL${[‰œËÜ ‘p’JÉgl?RìÐ2в ´ …ÜÏ:ôÀ©C áL[ÂSã*Q”¹¦KdBKÀ&GŽÁGæY`•É'¤ü© dôZ9o°›g—Ôð)Û »&fÅ£{õ©_pªˆÞèÕwt} ½Û8[~ñ˜-ù‘ÉgÌG Œ `ŠðÃC0då ›Sò“GÁ£/A²b¾ú e^éæG‘;u¼xÉ8’Ž)ͪ'°”˜2«"“&t"\bv‡RÍÌÜ ó2²–¹$äHQ4I`”æöõ·òÞn'Õ¥æG| †&fÅ£{g5Àb¤SEÔt¢ç·õ]_‚§žÍó4æµ:N‹’Ä$ñ&›ûäáÒ#à”|X9ÅØÁ:~ö‹¯9ìQC!·Ç:4LvZ†,»¤tÍ0 8e<нÈr`g·à`¸‰¬<³ôÂlf²É"\¸ýpHH©d)³!°y%#|F. MÌÊ¢î>ûΧŠè-<õ]_Bï6ΖßD¿\,~¡¸ÊˆÛ'C(%:G™€ŸœHgI’Ÿ‰ùYˆ )”ÉÀWš 137vGñˆ[7AàÂÐĬ,êÞü©_vªˆ&½iRßÑõ%Õë—ÔD<úUo÷’%_XöEV+`_äçÒãà—|L¡¸sŒ6åÞœ†Bn³æ­F0 ¤YB•x?°ÉÝB}f†-8˜d™ýû?û;zƒ‰É‹€Îf–Hñý |²Á@(ÁÈÜXã.‘t . MÌÊ¢î5”€äW]¦¾£ëKèÚÀRáMÄ£Tøâ%_X/Â¥£$ÿ^ÉçåúÝkñëøˆ§ùµÓÙZ‡ ¹Í:žŒªŸuÈš ÙµŸÒ´²õýÆ=æ1ã[´o?gi-+’¡ÄOòŸ30E69çñÓpC`§…G̹¸€041+‹º·¤Z㽺·³ÜÔwt} ›8_|ñ˜/ú±‹’ÿÑ>>®.¹½>?®7)=NÉǯƒ×ü„%ôc8°}ŒÛï‚nC!_Å:ÒÊVù É㹄*ñ~`‘I=XŸ uœ–^ 3óhŒZ|ÌàØ²ãcf3ÔXó>‘x. MÌÊ¢î5”€äT½e¥¾£ëKèÝÆÙò›ˆÇlÉ׈\”|aØ#\f’ü;%_ìn¬M@6íÜÞ†Bn³|!nóJà NÄèQ‚TâÀâ"ÃS2uÂ9q›2ëÀ—õ퇯կOцܯ!°»FÜ´!†&feQ÷ÚzÀHuªˆ†}:[T}G×—0[±Þ‘MÄ£w%w,QòÉ€©êº†%mýñ×o8cøÜ6Ø”†Bn³Ñ°8a Œ‰gÌâa(Iò«Àd"'û}¥ÏÓ|>…ûé¨Ù4¬U'4¶U•¢œ½¸€041+‹ºwQ”2øUDW¨ïèúº6°Txñ(~øEÉױޒ„—â’O6\;âÀìòº­{AloC!÷°. J<î×Cm2…× ,~ø]ˆO),Ï Êa ë :*"ÒÀÈ跞ˆÀznyŽŒÀ„¡‰YYÔ½ÓgßãT½…¤¾£ëKèÝÆÙò›ˆÇlÉ׈\”|Þ¾ebÅ)ðšÍ)ùØ;ÖAÈA<Ö!{¯éŠ˜£IXy9pÀP§?°pY6Ëú¤d1få–29ò¤¶½pRí‘áV\@š˜•EÝ;}ö1NѪCKåÔwt} ¥ºuo"]k¸oá»K¾ŽíóÝ À,ÊIv¥¡Ûc2‚¤4c0Ô‹S¥¤á €E‘ó£Ÿêì^^Ò ° Ï IÃ…Ì ÝE0⦠¸€041+»ëÞ†}:[T}G×—0[±Þ‘MÄ£w%w,wÉgE§ørÈ6˜„娓†Bn³`O| |Ô³F¨@R–µB!ô˜Vz¡ò ­Dž C³²»îí-3õ]_Bï6ΖßD´²!Û¯q‹¼½›…»YãF¸€0ЄYÿ¨Nˆ=ÛbO**!¶ÄëÈñi(ä^Ö1uíV²Ô³Žï}ÿ‡ì‚ƒ¨\ëÈ»;» Ðð)Û¥þÜ4ÌÊ6ÈŸTTB …Üfò§,ì‚-Ñ`,iá ,®ìã1b#ù¥s +g¿üê 6âÈ™FñK÷9  íP»(rS. aV¶‘˜“ŠJˆ‡-Á:r| ¹Í:`i‹°Ç-Ð!7¾1ÄüΟý]N3Fa'ëÀ¯ƒ±h†2’·w³pC`7«sܨ†0+dcTìIE%Äcԣ˱Ž÷o>äǨ¢›\6òEÖ Gúà,Û•?r¸ÅÌȈfŒ.ý¬ã¶:·Ð­Ñ핳àÿí| ¢Â2[>É× õ†À¶ªR”³†0+ÛÏIE%ÄÃC°Žw¯1s,ð仨²Ì“¯¤³c·ýk(䋬C¸DÚ·üÿ‚¼8â•,#š1ºô³r‚>¥Ü…­Ñö‹¯!6%Déð×!~awÓo¿ùªô|C`ý7œÇDàÂfeÑ:©¨„xØâ±;ë€]$K÷ÿ³÷>-–$WšwæjP~!}-Ä@Ñh)º¿ ´f@`Ѐ6=‹NA Z´j#©jH¤R#õ úÝ’²Z%RµRwHÐ…UUÙÙïÏ°ô?ÇÍýš»›Ýx.N„¹™]óã?þ\³cÇñÃ?î´n³asöm‰GA%÷Y“0+9ÆŽÓHOg±6»^¸cÎ6²Ä,…жÒÖ¶eÑ/|=Œ{÷)ìq‚èÛû#pÊ ×Ê6jÔ¨ªH=|õØu|áõ·x»ñšãÇxü°UÙÿ÷‡ÿÇK·mÌÜ QPÉ}ÖAé++é$Ë!ÒŒNzëÀ¡ÔˆÌ‚76ÖAï#l;Ï2HÒ/š•SØY×Uå 8eÐke½jTU¤¾zìÎ:R²ÑÕ/í×?2§ ’û¬ØÃxÎøÑaéi&ë 6)ƒ‘ †8€…%´Á»Cñ:ŽT}ýh >eG˲°½V7ókªŠÔÃïçÝY‡/ÞÆ¥•Üg°\88~øË7Y3k§–žtíÈdï_¾`)"Y:]E»%¶ÝRl]k N@ôZYC1úm6ª*R~W¦9õ°Æÿñ.à~¤;} *ù$ë`‡YV¶2ÁwÄ#ìûà!>ÆCÒñ4É:ž>}Ê%Ø÷Ͱd³*Òx‡:ІRÖ­¤+‰®.èB8ßÊ)*lÎåT§fN@ôZÙFÁU©‡¯•°Þ³ÁÏÁÖx>xÈ©/öJ¥•Üg7÷›¸sà_js"àД¾€uÀaà3xäÒLÆþÒ¸q[Fÿ’IL3ç[9E͹œêÔŒÀ (ƒ^+Û(X£ª"õðÕ£ÖË"ïGÜJIpàtãÕ+†RA%÷Yã¼Í‰Uâ–Â…±¾•¹î|gž%s¬ïÐÀâމfXBŽË:‡¯°IÎ¥á»9$Ä:ü‡H¥³(ø”ͺnÁÊz­ÓiªQU‘z8}JQ¬ƒ]É<üÓ7{„àÔ| }á‹—TrŸu0òÀ=2¸Á |ƒ4o|Ð0‹>ßHÓ™¬p ,F†?°J÷úòDŽS4È1C¬!>÷»¾F0±4ÍE àS6÷Ò¥êëµR I¿FUEêáwk ¬ƒ…*¼v㬠!&8ÅÒ—|Ò‚Jî³cü…NÐÜ/‡o„‡Ã¤FÊ4Òt&ëÀ©ƒYPb”ƒ5ÈP<4ü$®Ë… Xëûúñ}QØ5ú]mn‰À (ƒ^+Û(L£ª"õðÕ£Ö„¼ïx-ò¦#Mâ¯þ:ÕÜþSPÉ}Öa3,Ü)Ü€‘nŸÃx‚¥jH™FšÎd¶ ¢´lÍrEU`ÇÁo^ü:à*NåYEu]U®P½V¶Ñ«FUEêá«G%¬Ã¢’Úð>i~žïò)¨ä>ë`XÜ. 0 ß0bÀß8Û’’˜ÎdÁ7•î¿ò‹x´…›ß ²=:+Ø ÇË\Ûùùîç±ËS°öEQ†µeËl_¯•L Ž¬Ö¨ªH=ü~¯„u˜—×ïÇ•ž¾Ø+•TrŸu@! XáØÔ>ø”É:ŒNĵ*¶ |#æôù†åd²[9˰É+ÓU.‘`™mÜë×½?AËI,îå`ê¾uÐ×Ü8sàm«Îbe¨ç6õZÙ¦/U©‡¯•°ÂdñcŸ)ÛqŒS_ì•J *¹Ï:pÅ×÷α¿c”ƒü|ÖÁ:\è\œRQøD‚‘7W–½pZ çÅÀë8É—¯í ßøbeðÛ²”[ÀrêØ-»µÔµP~µ•jíôÚ©u0«Â .î>fa%v™j)h}ÖAi˜L?а8†QûËP†O$ðú N¤$øëWžõ,,ö„_¾>€'|ã‹•ÁGL¥B Ä:üލu``yÁñ«üFÔ« NÉô%_£´ =Ìb‡H\7œq'¸)Â:h9º’ZÂ' t@6‚GÇõµuJ){Â/_Û¾ñÅÊà#¦R!P b~GÔÀ:,^LJïü‰jÑÂíÝç _¼´ =Ìa6qÏŒ dÀbwÄU´Ç³šåH!ò‰„8E÷Ev·gÎ+ýú1éÅÀžðË×Çó„o|±2øˆ©TT‚€X‡ß5°$ îâK`îœúb¯TZÐæ°[!»ë0oRtC`ÒÃ"G„ó1èFD ‚5¬œ=á—ïXGÜtÖÁö$Z¢Žù¨T4Š€X‡ßq•°„dû†÷9Hø2¯WZÐæ°ŽUÇ:à p ;2㦠ÓH/ºX±ŽÆÓH/V†Ó¸}ÝÅÉ# Öáwq%¬ã•`݇ÝÕ}±W*-hwgË "*©¹ž’XÖÂà·+Ö1ˆgÓ™‹•¡é»–ð÷±¿¯+aŒí¯´fÓ¿ýNiA{˜Ã:l†Å‚ˆ²Ê•áô‚~[Ë95šàÝzØ×¶qÉùâdÅÀŠuLbÛ\…ÅÊÐÜJàû‰€X‡ßïõ°|mEÙ5›þíwJ ÚCŸu ßB’š×hŒ Fˆëu¼7içÖrNYi‹T¸pàônaQÎ÷Çë,V¬cÔVK+C«7,¹ïb~‡WÁ:Kec¼vœýÛ9²´ =ôY‡C*&‹ð>]# ïwFœâÒ!á´ÐúåÅÀŠu©Ò~}±2Tx/Iôëèc’æÔÀ:ì}VÎ^]°~“š¸¤Bn–.h›c¶†%nÚBB¬c3Åë_è„éVÁ§¬›r„Àîˆuø]Pë@B›U±}XØç}狽RiA{ØëRÛéúÇÁ–÷tJ)œ{Â/_Û¾ñÅÊà#¦R!P b~GTÂ:Ïg†Å–y²r¶`|*ÿö;¥ía‹¬ƒîý;lsO‚ÓRŸÅÀžðË×Çö„o|±2øˆ©TT‚€X‡ß•°ŽŽ+Eè\¥ZжÈ:÷QKXÌrýò¼ÏâœÅÀžðË×ó„o|±2øˆ©TT‚€X‡ß•°¶^-¸NÓ¿e§´ =l’u\>c’‹é-æ¹ †ìX ìâ—/W¤ Ö8ðæå©Yû@r‚È`Ö¹£É5-V†šoJ² ˆ¾†Ãl¿õÄW^Xâu˜GËþÐÎö°Öquqýâ¹|ñçç>°ô‘:,B)3,ü{óÑsj.v1ëØ]±42‹âž8ƒõC:ØÚ–™íJ¾%JºV» ábN÷ínœ‰ŽeŽá¯Å§ztÆžìŽÌë´‡5°øHšw® _f}«„1¤Î»ï=·¢°û[Ë+gwWìA„ fÔÕ‚Rå4Õ®ä9w§:B@¬Ã×Ý3»ŒáJʨ5!:-Sï»]¸bA{¸;ë È<02da>º¶- ±¿`wp0îqû„„ˆô××ŽÃÆÂ)=R䳨û<Öá#¿R¿Ù JÛ•|pt‰@ ßåýÕ t»³Š}ÞyM[Ú"E”zßÍꈂöpwÖÙH¯Eàx1”sp…Ÿ;3, Nc#‹ëC~1¤c n–ß®ä›A¤ 5.Öáô`%¬ƒØàßùùïMN<˜jqd^¯¨ =Üuä£&S«eof¸lžë°K~#~ÍÅÀŠuŒ»Ò±7ËoWòÍ Ò…šF ëpz°Ö‘Žl¯cðGºs¥Š ÚÆX‡Ml1¤ ÖÁŽ0»+Ö1Öuuì+å·+ùJ€¨ÙC@¬ÃïÐJXB²ÎÂÆö .Øôï½_ZÐÖÃ:ò è_<'(½}|ç,öX‘OŠ?±^X éâN,õÅv%/…€Ú9mÐpu8]\ ë¸ÙcH õ?x¸W쎂ö°Öq¬M  OýÛ}X EŠpî½_TÐÖÀ:˜(XÖ&“à Á©¬­v±¿!4z¹^X ì °Ög‰u¤ÏÚbeHQZT‹€X‡ß55°Þ,áw}ͯŸüôÇNÉô%_£´ =¬‚u€ýÓ7û©Ø®²°éÜ–U&@zœÛ.ë`m23V0=¶Ò³tÁ©–ÅéÐciWòcîZß½? áëpº»ÖÁkšAÔˆÖñá{œîÒkía ¬Ãø[œUyïÝ,tbL( q<ezyŽMÁ•D‹m—u …ñ"%Ðð]Þ_­`Xë+Ûc_…¿úÍ?³'KÁ÷ݬŽ(hk`Ü;DTá¤I€³ˆUƯƒš¤ù…îTžU´ØvYGÄG3, K,V†N;:u" Öá÷K%¬#î±ÎšYÞŒ1v‡/|ñÒ‚ö°ÖÁomÓßÚþf¾€];‚wÍ£³R/öX‡¼I;Z´X:íèTÔ‰€X‡ß/•°FõÓ`¬ÜôÅ^©´ =¬„u ^žã°|6rt©sùÌö‰ó)ʬ.X ì °¸\A$#ì‹!-ì•hWò½ÓuÛB × ‹Óe•°Öi†Y€Ëg¼µûQÊÇFìg¨ôå3›.±Ù“€°ó¹|Ƭ õ9˜g™¨ì´Ó+Zü¢9ÖÑ£LÆbHË\þˆVÚ•üˆ›ÖWïb~gWÂ:ìÈËîÓŸxÍBvøb¯TZÐV2Ö‹`bÅ~n“ð]5(Å£†æÔŸ¬<« +Ö1†óbHÇÜ,¿]É7ƒHj4|Ææou‘ð•°6Ãý€÷½ò zÌB¥ =¬u ùй¸r6®ÄÄ*Ã7¬ÔvºgaÑ`å¹™‹ëƒz1¤c n–ß®ä›A¤ 5.Öáô` ¬/^޼_¢œï¾w·{ÌÜ QÐÖÀ:€`ïbD¼<ïàœBj¬#èÀµÃ©œ~1'½X±Ž1xC:ÖàfùíJ¾DºPÓ ábNÖÀ:ì}Ç‚Y6[çÇ5™jqd^¯¨ =¬u„xÎØÃ7ÏÃf¾ÎÈû@3¢;‡U«971°bcP/†t¬ÁÍòÛ•|3ˆt¡¦@ÃÅ:œ¬u ž­ÓÄ©àµÏ|Ž#üBßãSÐÖÀ:€˜†9ˆ’ˆ(ƒè‚?Çëßú¥Tfí`µ™‹ëC{1¤c n–ß®ä›A¤ 5.Öáô`%¬ m¯þâQ€+£#ózEía%¬¬p•±($üu †A¥àg1°bc½°Ò±7ËoWòÍ Ò…šF ëpz°ÖÁö¦?ÓDåõ·Ë§ =¬„uØü.LÇÕE8ÿtza¼âì’ÅÀžëHB L—]y1¤Ó2¬\£]ÉWFÍŸh¸X‡Ó—û³\.Ÿ¿‘0ø„~L çfŽ.*h+aÌUá¡a[}ÿËŸºZ¹Û<ë˜eNåÅý”Û@»’{çúþý@ ëpºzwÖß1:<ä Á‚ÿ“ܹ#‹ ÚÃzXy«1Ö±v/,¶uÖ1+ʬʋ!=ò9þëíJ~ü½«…û€.Öátôî¬ÿö^Á‰ÇÒÁ³oÒœðÝÎ-**h«`W ™zŽ9ˆ®Ý ‹mšu`yRüý(³*£Ý‹!]ôd”üR»’—DAm.h¸X‡Ó½»³“-Ý—“ųØê]z­ =¬uðÊI¼5 pEX¦ZeX¯{¬#3бŽÌÊtâbHئ¨]É·ÁGWi4|—÷W+¸UÂ:¾ðú[¶`ÜÞþÑ7xWî`A{XëC›U! GÜ¿Þv½^X lÓ¬¨Ñäü(³*/†ÔQ€mŠÚ•||t•Ö@ÃÅ:œN¬„u0Â×­ðîûßßý#ózEía%¬T™a±•³“›×öÂѳ]‹mžuÌ 2+^ÊbH×{|2[nWòÌTµ{Ž.Öáè@%¬#•eŒŸéϤß*’.h+aaÏzVÝ~&CvÜV $Ðz©™ðký¸Ïb`[gÀÈæM®. uþgVåÅú2lPڮ䀣Kœh¸X‡Ó²Ž(-oœ§Z ÚÃJXËfßxã›Ò¥ãEza1°­³`‡9Û W Ð¬Pv>s*/†Ô¹þ6EíJ¾ >ºJë ábN'Šu¤à´‡»³V³ÚÆßøÖKC±¥7î¤Å:~ûÎoÒŸÛ|“bÛÙ°r™IC–0sê@=«rA]uDZ£¨]É×@Cmžb~ŸŠu¤ø´‡»³sç`I2~8uàY ‰³Wé;i±Ž”rÎg¶™r\¹LHæ Ú³*ÓBA]”g½Ìv%_µ|Jˆuø½)Ö‘âSÐîÎ:ì¾xE"‰¥ß¿|Á[/ÿwº}K¬c1ë¸îvn ïð'3U¹˜žU™oÔÕ(Ã6‰v%ß]¥uÄ:üëHñ)h+a½ñ븾þݯ¾Ë[/½ßœ´XÇbÖÁ°€»áLà”À:FöšU™ êjެӮäAPS'Œ€X‡ß¹b)>ía%¬ƒQý¸nÂlKz¿9i±ŽÅ¬xƒ;ǃ‡ì€c›àøKÂgU.¨«9jP°N»’AM0b~çŠu¤ø´‡•°Žôî` “‹7Óú–ó2Îúù³r çùüÙgyïϺ•+QlF0Ì£§¾Sͬʋ!‹dñúíJ^ 5x’ ás'²O‡±›ªÄ8Û7dìvŽÌ/hëaÿyþ=Ö³p˜‡Ïíæ¿q´dÞדڋmu$’·>Ý|;O"«„Œ[ü+/†t°µ-3Û•|K”t­v@ÃÅ:œîÛŸuÁàÅsÿñ{¼Ùé¾c{éK´‡•°óå`Ÿñ þmÞÁÌvl± ¼îYðâTžU´ØæYGÂØçþ‡)€9¤#è)ÿÅÎê¸5*·+ùh¨ÍÓC ëpºuwÖ±öëν÷‹ ÚÃXƒE¼¹ðÀãé¯"¢mæ{ø N)±LÃÒΫ ¡r¡Ïb`[gàÏ-Š$|H) ü8ê–èÀ¿R§ÍmŠÚ•||t•Ö@ÃÅ:œNÜuàiÀ>܃íW,í½;)QTÐÖÀ:âkxòÓCBx©Å—`ŒàT¼-^ )FÂEƯç$‹´-úu0RA$RÛ=™á ‚³Ùð…)˜£ügØb¶‹!uÚܦ¨]É·ÁGWi4\¬ÃéÄÝY‡É–†•ÐN÷EÓü(Uúàc^sàyÃ:>|ÓþãÀ›_G¼Fm“¸»­â4ÖñjTRëˆü¡Ð V€Øó×.¦†/Œ"ÒY7OëÕ…ï‡Óî»»]ÉCª"!ëˆP &*aXiLŸ}ûKXìAi×Î,hkë.æV¾ø©üˆþÕoþ9ûøI&†qœ$³¾_m1°Žu0¬dq9làÂþþÛx3,a$êÑM ’pØJ[ÕÅ:mnSÔ®äÛ࣫´ŽÞÿq×úM”¿ÖÁx2‡}´Ó}gp£sšó[;j¨†ŸÛÎX6 ùÎÏ‹üDX0›MQü¦(]ü¢i”u r~þŸN3ÕȬŠ¡×ܱ¦Åʶef»’o‰’®Õ.b~ßUÂ:ÒE‚ö#1ˆí/3ôolQiA{XÉX‡msÏïh>áo²¤b¡—ç 쇱ý—çö­áj3sÛ<ëHt<ó× S™™/æÅ:mnSÔ®äÛ࣫´Ž®±§+aL„e›¯~dægþ«yëž´‡•°€Å³lscâTÀ??±9øâ‚ jǺg1°­³–Ê!{—ÏàrþÊY¦Àü¥Í)¼‹!MÙ%Ý®ä»À¥‹6‡.ÖáôZ%¬ÃÞtØäð+ûðó%8$°ÞÓ¾xQA{X ë0`ØÏYŒ þTã÷8#üRÃM‹muÉOâµÐ #“&6¾ÄjÖE $œP6ãŸÅŽ7¹QI»’o.Ó8h¸X‡Ó‡µ°ŽÇOl%f™!Lnx>xÈzOGøâEía%¬ƒ÷]îbÌ« ÇýÃP5×G^úE@^ lë¬"Ÿ†9°óë°5/àà ^ü‘ÅéÐciWòcîZß½? ábNwWÂ:¢ /k³½Øj,0+/á‹´‡5°^Ù€y·óú: %õ>T ¦Ù7He^ ¼"·„Þ÷fd,¶iÖa!PRæöî{ÏQCÿ9ð~ý[? kD…¾sl×bHØ2³]É·DI×j4ÜyrÛ½¯R’WÂ:p'¸ßxtf¿ ¹AÖ_Üy––ºa·‚ö°ÖÁû æ0¹ç^p~˜36á Á©‹ÖŒÂÅÀ6Í:°<‹·Æ‡|jÇ_PuP#¬Êo|Ó*P90ƃðàWC:ØÚ–™íJ¾%JºV» ábN÷UÂ:0°¼ñ%à'ÞŽŸ‚ö°Ö’ŒêÛJLþŽ-Æ´à|?ÒcÄ aA-¶iÖPLo°¯}æs蹃Þ×ÿñ_âÒf<è §òbH6·)jWòmðÑUZG ëp:±ÖñøIô(p¤]»¨ =¬„u¤ˆ1p4¸Ó†ô)Mš *é×I/¶uÖhU\e ~70ŽL]ÅÁ vÏñŸˆÅÓE¾Û®äEn_œ<h¸X‡ÓË•°‚1â±Ï<8Gð%8Œó3Îì_¼¨ =¬„u0oUpìbÀ{¬# ‹ĴWš—fÝ ËlÙvÙ]ÀBýÅÞ]k§T»’ï˜.Ûh¸X‡Óg•°Fž±Ãvàºo 2µ†¥˜”ÓÜØ¤‡÷®Ôo±CÈGV-Zü¢91Öa sSÃS-—ÏÐ||?ìð)÷bHWíèœÆÛ•<çîTGˆuø:P ëSÞÎØw,΃“&G¬c1ë€o„‡_$8x—ÙZ•¾J„a¥Á—ÏŠŒ8-~ÑÜ+Öå¾7Ÿ0½B‚x5ýžŠ9‹!-ì•hWò½ÓuÛB@¬Ãï¯JX/ÄèGGd0N*"Ö±˜uà˜ÁÁÞ+,ÆÄG—t`tx3y>Ø™ê‚~p@6œBùìêkÑdéâÍýa,r¡wè&Û 8ìƒóà!·?†íbHÇÜ,¿]É7ƒHj±¿û*aX^¯&jˆqpõç]‰íõå/[ZÐVâ×ÁôJ\Œiïµ± GÐÆ}ŽqwB·™+‘8/öþ°î4¨ýÕ…±Ž~¬N,†´ÓÎö§íJ¾=Vºb‹ ácf¶ÅÛ).s%¬Ã6d'dÄÅŸŸ3Îì¯,Bl° =¬„uô’±»Aöø0:ï·Ÿ€c@?ì(Â7싽?¬Ã‚«0ÖD˜‚ã® ú×ï ˜³ÒØÂ^‰v%ß 1]·-Ðp±§Ë*a ,‡H‡*øË[o—OA{X ëØH` 8 Ü;4ϲ拽?¬ð-d:ÌÖ÷ ¿œY ©Óæ6EíJ¾ >ºJë ábN'VÂ:0þÊŽoIG않 ÚÃJXG ÀZ8FïPJ‹ÖN/ö$YGðÙǸº€%âGMˆ÷åõ¨SýµÒµûz²ýv%Ÿ¼5U €†‹u8šP 븺À}ÑÖ̆ØÛþø´‡²Žx›7.ñ|ýÄb`OŠu\]„…B×û Žæ¡ÿa£œÃg>3\ éú½=q…v%Ÿ¸1 h¸X‡£ •°˜ÆÍôÊaž…pÐŽÌë´‡bi7-¶uÖ‘À‰× °m|SX:iæV8X7–<f;ÒÓŦì’nWò]àÒE›C ëpz­ÖaîúLjã½Ï²Ù0»ýàaRºù§ =ëH{o1°²|1XaŠbÆ/â⬙4/nÕæÊk>ii'½ÒN;ÛŸ¶+ùöXéŠ-"€†‹u8Wëˆþ¶fiad:b¯TTЊu¤}´ØFYšŒ‡ÁÃJd‹ÒF0Æw3Š’ªý»ï=O1ì¤CÚigûÓv%ß+]±EÐp±§ãª`lÈþà!SÞÆ:&÷øvnçÈ¢‚öP¬#í‹ÅÀ6Ê:¯`°Âü7lìÂrÖaaÁ~öí°÷“2üµXy)Œiz1¤i#»¤Û•|¸tÑæ@ÃÅ:œ^«u +:1Ȭ„{0Îìȼ^QA{(Ö‘vÓb`eé½Çô¤_GÜ€¨¼a€ñÚ\ i”g¯D»’ï…˜®Ûh¸X‡Óe•°~²`½'øIˆíåw¢É\ú7ü´‡5³ŽÑ•›«A½ØSbÀ~/ejV7ÛAz‡‘Z!{1¤N›Ûµ+ù6øè*­#€†‹u8X ë”7Žÿsoð[Çd´‡•²ÜØÝ–ŸÅÀž븺€<þ<µy=yÃîÀ·?pÍbHo›ßí»’ï™.Üh¸X‡Ócb)8ía%¬#ˆ¸ ~ÂÜwH¡(›^ ló¬ã°y½Í¾?vÇ‚ñ›Msn»Ìé…Å:mnSÔ®äÛ࣫´Ž.Öát¢XG NA{¸?ë8¼¹xßñ :,C¾º ±ñØQÄv1°­³ÃêŸC—˜1 ¬oˆQ""z‡¿ã ‚MXr;þY éx“•´+ùFé2#€†‹u8}(Ö‘‚SÐîË:øÕÌûËcÆ™½×ÒûÝ,½ØÖYD".€5oRî¨;´¿&œ¨9HàÚdÅ"Ø÷듳ÒÁÖ¶ÌlWò-QÒµÚE ëpºO¬#§ =Ü—uØk ÷E^añK$Üe)eÓ‹m”u°ô•á 0$^»Ç˜Ìvqꉯ}í«¨U¶…´ŽíZ iÙž]ÐZ»’/¸Y}å"€†;Oî=¤sËb) íᾬ#½©˜æ .ÖñÛw~sä7˜4)\¨‰¹Á CL0›Cá4vG?Ášñ¿Žëë°Ó½K êj_’UsÚ•|UXÔøÉ Öáw¥XGŠOA{X!ë?Ÿ?Iïw³ôb`ëˆQIü•Ã%æþa2µf1¤›uúØ…Ú•|쎔/RÐðÉ&iýû–®™uð–ôCflö°"Öñò<®Üt†÷‹ƒ™6¸ØFY8ÇÈ1Á.öŸþÊ/RXœ4 „úásuvaî}CÚkiëŒv%ß)]¯MÐp±§ëjd·‚#¶²À¹¯eEía%¬D0VÏÁP¿¿,bh9ßZ l£¬#“Ì:cób‹!ͼîzÕÚ•|=LÔò)!€†‹u8Z 똵!¸s;G´‡•°€e„Ÿe›þÊÍ#q›üúb`OuÜ.•µõË“Xu*ˆutÑ©¨±¿ƒöe¸Û…¸IWüÏßÜ¿£cJ¿û­‚u€e¬ÞÄsVnöå/›³ØÖY‡9‘âjƒNßÿò'ç+Ö11Õû" Öáã¿/ëÀe¾]ïr6÷oçÈÒÅ/Çþu÷eƒ+7áu¬ÜŒRÕ—¿lÎb`[g¶‰zζ†6Ï…·Ò,lÅ:fÁ¥ÊB`wÄ:ü.Ø—u0ìo¿Äm Àþ’ƒ}öÅ^©tñ˱/Ͼ¬ÃVn2Ž4kåfÿ.Jå,¶uÖñŸÿhTÚvs›Üý­¸XGåšëð{g_Ö1(›M ­¹øåØl_Öq~þ†5Â1gåfÿ.Jå,¶uÖ€qÝPLÌBU¬c\ª,vG@¬Ãï‚ YCÐ ~ú·™YºøåØo_ÖqüÊÍþ“³Ø`ÇàÆwÅ:ŽP_# Öá^ë`:€¸a›Ô>‹_Ž}y÷e}yöÍY ì½bî­Ë Ä´ïwßbHûMmœÓ®ä¥Ë5Š®•³NßUÂ:‚£#1%lRàÁCÛÃÂ{¥¢‚öP¬#í£ÅÀÞ+Ö¡îSQZ4Š€X‡ßq•°X}áõ·,Š#Ó+0Ü`óÏâ—c_ÒÝYÌ-Œ½<ïÊvù,ýñínþšç‹½'¬ÃØ¢ö<Úé~MMTÛB`uÄ:|ˆ÷gWŒ!coq€4Qgæ”×/ù¥‹_Ž}avg¬ž`숷˜MZÁ@xµ/‚òYRÑ—y½œÅÀÞÖÁ"¦QâN÷oÛéÞé‘Å:mnSÔ®äÛ࣫´Ž®§÷eüâ&x&â±Ôâgßþ¡$â Ák±­°}„wgˆÄJd6þ¸™·º]ÒÂ~÷Ç™G’Å/š{Â:L´Ó}ÿ9RŽh±¿ËöeV‚ã_üTØ+ÄÆ–Ipøb¯TºøåØ—§ÖaRÁ1b8”íù†É°Ø{Å:ÒîÍÓÉYl»Ò¾®nœÓ®ä¥Ë5Š®±§ïöeŒó3°Ü9ø1.Ö;øEÜœJ/~ÑÜ+ÖÁ< üÐ>„wcÂÅyrCê´¹MQ»’oƒ®Ò:b~îË:Æd\*8V¹`~A{XÏXGA|7µضYÇí¾ow¸õsîÊ®¯/ŸÅ3HÜé>f¦‰Å¦ì’nWò]àÒE›C oî‡á– ×É:¶D ½VA{(ÖQØvYÇÛ?úFpUâÓ°ãòsˆ),4޾ý`5,§e—NMN êj¿ñUsÚ•|UXÔøÉ Öáwåþ¬cü×ß²ÁýûõK ÚC±ŽêÅÀ¶Ë:Þxã›ÑUÉ–Âê!×a‰Rü«m#fÄ—§ŸüôÇ)˜–^ i¿©sÚ•|c t¹F@Ã5ÖáôÝî¬/…3#KÀC^<çG¶ßGøâEí¡XGÚ;‹mšuà˜aëaYIDbÒa‰•\TcÍ,܃5Î<<¤5Ö‘ê’ÒB rÄ:üÚuà>gf–_‚Ð Ö³ð×bJµÀñá÷ïkYéâ—cÿrb)&‹m—u@§9˜[ae ³-$¸—°$|üsy}£å ãÎlá³XÇ8f*Õ! ÖáwÉî¬ñl§ l Ÿð—_…љߗ¿léâ—c_ ±Ž“ÅÀ¶Ë:˜aákÃÙö÷Ñ;)£ç¾V÷oÆ7|Ã*ãS:¸Þy1¤i¿ì’nWò]àÒE›C × ‹Ók5°Ž(fyÐÀÆ k' ÚC±Ž´³Û.ë@²cìŽ=”3µÚÆ7`Ý>91`CšöË.év%ß.]´9Ðp±§×ªbŽœÛ´‡bi—-¶]Öñô׿µá»LÊ\ŒŠ°`6ÅÍI/†Ôis›¢v%ß]¥uÐp±§Å:Rp ÚC±Ž"À¶Ë:ˆößßb¯¿06E)”ö–t±žåŇï¥Õ,]PWû¯šÓ®ä«Â¢ÆO±¿+Å:R| ÚC±Ž"À¶Ë:pê0? ¸äŸRæPRX:iJ‰ÎÁ¤Lz)oÒP:5# Öá÷ŽXGŠXGŠFÁôb`›faß½ƒ+©9l„S—u¼ö™ÏQ¿ŽàVÍ–ÇO–¯£ *ª)!°6b>Âb)>‹_Ži#–ÖXGŠÉb`Ûe¯ë,ý&Òa¾ xw°Œâ‘ÂÒIS“¯X& †JHó]±ŽP:5# Öá÷ŽXGŠÏâ—cÚˆ¥Å:RLÛ.ë`Íìó·¾s°!óè¸ÙZ%…&I3¬q~þË Á)iV‘CW’Z7ÉÅö›Ú8§]É7J—k4\Þ¤N߉u¤à´‡bE€m—uXDt¦HX9kQI 4Êþõ),4ü¿Žë—çŒðF êjGŒµOÛ•|mdÔþi Öá÷£XGŠOA{(ÖQضYÇã'qÙ,!8ðÖ ‚G K'Íb[˜Ã#!6ßùùï;ÒÓ‚ºš6»Aº]É7G—8Ä:üNëHñ)hÅ:ŠÛ.ë òF'Ø „¡–~š¯àÑAµÉÀuµ/ƪ9íJ¾*,jüdëð»R¬#ŧ =ë(l»¬#½ýü4kf™ˆá ÑÝ‘¶SPWÓf7H·+ùàè'€€X‡ß‰b)>í¡XG`ïë€i˜ë©­· >㟂º:~‘UJÚ•|8ÔèÉ! Öáw©XGŠOA{(ÖQØûÃ:ð Å—ƒ‰ö¨%, NÉLaLÓu5mvƒt»’oŽ.qˆuø(Ö‘âSЊuöþ°îÔ–ÊþÕ_ÕtpJf cš.¨«i³¤Û•|pt‰@@¬ÃïD±ŽŸ‚öP¬£°÷ˆu\]C3ëÁX¬ƒ=\8eæ±OA]»ÄJùíJ¾ jöÄëð;T¬#ŧ =ë(ìýaÀÅê˜Æà z¤vÒuµÓòÚ§íJ¾62jÿ4ëðûñéÓ§„3ZãøÚ×¾ ¥É<°±¼¦3+¯Z͇+¿!×@•6ï°÷Šu0²ñõ½ ¬ÁHa w o{Ñ¢‡í¾»Û•<ÿñWÍûŒ€XGý½¿øÍRÿ­í+á »ø`}ød2hPü€õÍwüâyÜsv™2p-Û¥óõvßÝíJÞé AÄ:a©*sñ›¥ª»¨P˜vñ-ÔÀ:؇%De«ÙÃn³Dá Úù\=æ\úßj÷ÝÝ®äý^PŽè# ÖÑǤ¶œÅo–Ún¤6yNØÅ·°;ë`ËØ‚M”0WÂÔ Üãû_þä\%똋˜ê }ëØÿœ«/~³ä4~Ÿëœ°‹oawÖñù³Ï~áõ·Rõ³/>|/ÍœL‹uLB¤ B *Ä:ªêŽAa¿Y[SfDà€]| »³Ž×>ó¹ßýê»±/H` Ú4s2mßêWkwž¢]Éû½ !ÐG@¬£Im9‹ß,µÝHmòœ°‹oawÖÓé?õà?Ï¿µâí}ƒ9'G¬™&Ä:R4”õ# ÖQ-~³ÔkûJxÀ.¾…X‡ù‘žŸÿÁÔ€Ó¯ÿã¿ÌU ±Ž¹ˆ©¾Ø±Ž}ñϹúâ7KNã÷¹Î »ø2Y«k‚`± [­qüìÛ_bZÄ_l›¹rö_ãDÊß°nåÅs6åtî@ÚËŒ t¥¯ÆíÎS´+y¿”#úˆuô1©-gñ›¥¶©Mžvñ-ä°«Y™õHÖºB?â‘É:P¦WhŸ9ˆ¹·ñ7¯E®.ôADå.\XR£Ýww»’'ð+)Fë…¦š‚Åo–jî RANØÅ·Ã:ð»¸áÄÓxü$· Ä!™¬Ã6R!Ð(kf9>ý•_Ð8c)Ž®@Qâ “Rß©Üî»»]ÉîP‘ˆˆuD(ªM,~³T{G•vÀ.¾…IÖAãð§¿þ-S-ßùùï{0:1Ú4“uØF*$Û·q®Øß¼~¨ˆítO”ëËgîÃQ¤vßÝíJît‡Š„@D@¬#BQmbñ›¥Ú;ªD°vñ-L²È€õ­ýÉc#˜Ž~9™¬ƒ–á-M e ?!? n¢˜~ú¯ÙAf§fzÚî»»]ÉSü•cˆuŒ!SOþâ7K=·P§$'ìâ[˜dö®çoŸZë`»ù~Q>ë DXØ?% Ž_-÷÷pÁmƒbæ¿AÚ )æèU»ïîv%wºCEB " Ö¡¨6±øÍRíU"Ø »ø&Y‡u0“ÒY´‚ëEÖñ§>fì‚ößùû¿à0·Õ¿ù§ÏÔ ùud¥jB 6Ä:j둾<‹ß,ý¦”“"pÀ.¾…IÖa^ ˆV†5cn”cpæÅêdΰÐŒZ'ÒƒŸª¹•¦½ã§Y0Ë.NvG Ú•Üé ˆ€XG„¢ÚÄâ7KµwT‰`'ìâ[˜dPˆ×¿õãOŸ>5FÙ9ŸÀI,§ÿ7“uàjK_q(帼~Ÿe³i¨Òa%9Ô Õ’©™Áší¾»Û•|°#”):ˆut©ðtñ›¥Â{©J¤vñ-ä°ÁB…ˆ.8c0Â0¶zÅH&ë Zœ^±I6œ…Òxrù W#B|×§(í¾»Û•Üë;• [Ä:n‘¨÷ÿâ7K½·T‡d'ìâ[Èd b“âË«'GÁؤaî!ú“,v@o|Öæw?Á”©*C<œ¨bí¾»Û•¼ŽÇZRÔŽ€XGí=tØ‹{‹ý¯_Ô¶$\üÊ®ç6ßBë‹M ÷°aÁ¿™c¬I!8,‚á µ±Í?q°…“¼ûÞs«`Þ¤ÜþXývßÝíJ>ÖÊ)b)u¦íÍb¯ ·u”B€‰èÙ€¬Ñƒ÷þ2FšÃ:œØ¤ñÈdøu@6؇…u+,fa ±¿÷a!i–~}M5ˆ=¤ø„p:Xß*´ûînWò:í§¤ª ±ŽÚz¤//DÞ,:„ÀqÛÖ¾òŒåL²Ž7ßü–Í€üÕ_Õb“ò7†#ÈgvcÌ»#“u¼ñÆ7m§{¸Çù¦] ¾À0+J‰PÊís]›ë!Ái¿rÌi÷ÝÝ®ä|%„€ƒ€X‡N%EÆ:ˆ×Tê7¾ÚIht”#{¢N²‹åèL£ÄµÑÅ´S!Ÿu0X8LÜäå°„¶%,F% •ÓÃu=m÷ÝÝ®ä ôP_¹‡ˆuÔßé6ŠÎoÃúE•„­ 0É:I`„­ë Nmý,¦£_DN&ëÀ)‡R¦HX6km mÀ´Ôêð—©|Pû•cN»ïîv%à+!Ä:p*)Â’ó 먤;NCŒIÖac(^‡Z°¤ÅfCŽd´ƒ©w^]Ü¥KµûînWòãzLß¾/ˆuÔßÓbõ÷QsN²Ž"Œ0øpâzÁ_|*n\Lé×ÁˆÅ#eÐÃÐÃk2ä-- [ÓÞ¦¿Òî»»]É;B™B ƒ€XG OÅ:*ì”ÖEšdh9Ž2²ë¦6ÊÁ_8Ig $žfΰ0vGÍëÏÃÎõ‡¿“›×Ãyòcü:äôB»ïîv%wºCEB " Ö¡¨6!ÖQm×´+XëÀqÔ"wE²ÎO9F?‘É:lLܶ>gózó'1·j‰c]Ðî»»]ÉÇúBùB E@¬#E£Î´XGýÒ´T9¬Ã>ŸÐ ;X9 é34'Ÿu°@§ÐüÍë‘ÄŒ ûÕ’@$§ Ú}w·+¹Ó*±ŽEµ ±Žj»¦]Á&Y}ìoÊ4Òt&ë`†‚}Ùp ÉÀ…¿,…:1,XLŒuA»ïîv%ë å ±Ž:ÓbuöKÓRM²Ž%ì•é•ÃÎ)–“24É:‡nÛ¯°£ÜH¶ûînWò5úQmžbõ÷©XGý}Ôœ„Y¬ƒø]GŽÈ+M2S¦‘¦3Y±Fû›ÆÆqãñl÷ÝÝ®äÇ÷šZ¸ˆuÔßËbõ÷QsN²ÆâÂÑ?p®°Ì”i¤éLÖÁ ëdm†Åþ²D’S ÉvßÝíJ^ªïÔÎi# ÖQÿŠuÔßGÍI8É:¿èÁñ»¿…‹…n)]¬ãúZ¬£¹§IÏB@¬c\»TëØöÓ¾è$ëXÛ¯ã&^Çõ5+a<mÛ¼¾ìí¾»Û•¼Tß©ÓF@¬£þþ먿š“p.ë;Ò×?IgUÒtæ Õ8ðî ðS-þë0-ëhîi’À³ë˜×.•;¬crÍà.Bê¢m!0É:ðë`É郇ßûÛ¿dÎ%Ý«7ei:“u0ÖA›|‘Å,~Žs©ü:Ð"±Ž¶%I;±Ž¹ˆmSßv·Ç€s9ciúéÓ§Ûˆ¡«œ*“¬Ãˆs1<)ƒŽ;G$™¬#ìóòèŒ6?øà}@&6i`8ÎJÞî»»]ÉKõÚ9mÄ:ªí_¶ûä@¼”0 N&9ÕŠ-Áš@ “u— $iÜ“…Ð^l;¶õõ3Yƒï¾÷œÀ¤6vgAJù;úéïõÖÏI¾Üî»»]Éø•£ˆuŒB³wA$Æ:‘F¢HEö–N×oIÖÑYÃÓ@÷,>Ùy-nt™¬ÃVÎ2«bËfÙúKŒí9KÃ,q˜†—ÏÆ*[Ç´ûînWò¶ I¿b[!=û:F6àq¬ƒ‰,¿ÑÙÍé B A`’u f‘c &:d#žf²Ž0Ãrˆtzã§jëgGVΚ0ÐŒx05Ãדê&Û}w·+y·t.†ëB¥–<Ùˆ¬ÃF?>úè£Zä“Í"0É:¨ÐY´Ò94£“ÈdL¦oÕà ÆËsæYœ5,°¶ŠÃ锿|‹ÄßüÓ¿‹u4«}ü^# ÖQs÷cl!DNà/Æœ¿š–l­ 0É:à¶bŃ;|ÃN3YLƒ¹6…Ì0pb¡N¤M* ÿ̶ÀÕw=mwÄ ]ÉûN™B ƒ€XGªNÖ°áŽø÷'?ýqUJ˜FÈaƒ¤b23“uÀ¨Y9 zÿõñ‡Œ]0pÁìIgƒ—Ê\:Œ·<:cT„±»K')œ:˜ˆ±ÈüÅýŸR­aIRR4ƒ€XGý]e+g1¼õ‹* [A`wÖÁt!3,À'MX–‚óÆ €(ˆ—~p%…l„4‡âu ‚¥L!P7bu÷Ït w(LG=ÕŠ»³ˆ„9ˆâ#jÇ?üÑÀ_¸žþïïþGSâ±3<Q Á=ÜM[Ú§hWòV”_rî‹€X‡?ø`Ÿ×8ø­—ß,ÃÑù•W­éÃ¥ÒV@Iœ½T&—Ç:òWÎ2vhzŒ _p¹ço}‰Iâ0oÓy“¶¢ˆ’óþ! Öá÷ùîÆÙoãRý Ûðõ.·»b3ÖÑY“Lš„–á'¬œµ¨¤ Œ°u‹ƒO»ºÚ®äNw¨HDÄ:"ƒ‰Ýó T{eÊî…|ñëî®ØPfX¾°XaÜ ó&„ ¼ÓÀ:?‰ ,¶e혈µÐ®®¶+ù`ß)StëèÒ9ÝÝ8wäÙ÷Töp_ü ^}wņ9À1옼/§S?ýÀ@úHs:évuµ]É;] S!0ˆ€XÇ ,1swã%©!!{XC/‘áä»]]mWò"š©FN±¿‹OÞ8û·ß)•=ìÒîéÉ+v»ºÚ®äí>’|KÄ:|´OÞ8û·ß)•=ìÒîéÉ+v»ºÚ®äí>’|KÄ:|´OÞ8û·ß)•=ìÒîéÉ+v»ºÚ®äí>’|KÄ:|´ë1Î?ûö—ˆíK»v©ìáÚoÖ~=нÒ-·««íJ¾RWªÙC@¬ÃïÐzŒ3áIY?èK»v©ìáÚoÖ~=нÒ-·««íJ¾RWªÙC@¬ÃïÐýóËók¶ÄzñÖ¢*ÙéÕpXÿ^Ž/•=<ÃJZØ_±W¢]]mWò•»TÍŸb~Gînœ‰MèOâ5b3†ðŒ‡›Pøb¯T*{¸°Û7»»b/¹åÏmÛYöœüz»ºÚ®ä“¢ BÄ:|5ØÝ8¿¸º&>[N°!8{t’à”Ì]>²‡»À¾ÆEwWì¹7Å>,# û°v›%"z÷ÿ´««íJ>Þ*wˆuÜa1”ªÇ8˯c¨”·z› DÆ.†n‡™ÄÁûaËúØvÊÍADS¸Ç÷¿üÉÁÊ–Ùî»»]ÉîP‘ˆˆuD(õçïüü÷OýÛA!7Ë”=Ü êµ/TƒbÃ4²#Ì>:#ÁÁ®÷x1unÿógŸe /Í$F:ßzñá{ifšnWWÛ•<Å_i!0†€XÇ2–_ƒqö%ܲTöpK´W½ÖîŠÍŒ ´MÜl+Á`4ÃèÇÅŸ_!¯}æs¿ûÕwS@lƒZ¶ M3Ót»ºÚ®ä)þJ 1Ä:Ʊüݳ/ÞÆ¥²‡¾ÞåvWlÈã}%Ûy¶ã/Íê­/~êAêDJìš0Ö1îàÔ®®¶+ùzꪖO ±¿7w7ξx—Ên øz—kK±aÁ‰ôÑÙùù Ò_ÿÇqðiWWÛ•Üé ˆ€XG„b0Ñ–q¼…‚™²‡ÁÜ·©¶Û–nñ7nL®äjWWÛ•|_•ÖÕ[A@¬Ãï©¶Œ³/Ç—Êa%-Ô£ØÌ›àãiÇ—£ ÔËs«FÍéʇ˜Ý9×SÖHGIÌ…ˆuøÀÕcœ}9·)•=Üç ®R‰bã#j XÂʃ‡—шkU^ÿï!XÞ§$XíÂrÚX¡ŸhWWÛ•¼ß Ê}Ä:ú˜¤9•çT¤Ó²‡;‚_öÒU(öÕä%±xr°€…Sý[Â4˜UáÃ$ u>ý•_Pùz|k€vuµ]Éû§!ÐG@¬£IšS…qŽacÇÍl¬.ÅjWáÔùÄšV§súêe_Å£á³ÛV¿þéƒ G;åoVVÎv‚Õ@<¬l_oWWÛ•¼ßqÊ}Ä:ú˜¤95ç ÏÕsÙ 2s¸#©¬·i¬1amúû{û—œÞ– ü'LSXHXÈCˆHÚ¨t›%{x‹DóÿkPlF-ÒY @:‚ƒíÐäôÿø6߬lÕÚÕÕv%O;Hi!0†€XÇ2–_ƒqF’@Øî+¿à ѱÀ[À3÷m•Ã$¸Ë:(å`°:ì+w˜[ï´–žÊ¦h4®D±M÷l8%„] ¢j‘H!ÒÔ´ðéNek¡]]mWòÁ¾S¦è ÖѤsZƒqþ¯?Ä8Ýdc¨™S2;¢Ú© SG;ÂJs\>‹_¬O¬iL=3æÁ¶?:¬f™²‡8mÕ ØŒuàË=6%ažƒHþÍ?ý{`Å·tzÐ$~·]]mWò¾BÀA@¬Ã‡¢Œsg¾»sÚ‘ßF­mì‚á Ò:ñÔøL:9þî{¯D¢Ž5-!{ؤÝÓû½« †/Â4ßø²°hF9,j‡1d&ÃWÆ?íêj»’÷†J„ÀbwX ¥j0ÎF$pã7w‹ýÑG É<@ ᇡ™h†/øU8\óúšiq*Ó2³ä,Nä¯CQhDöp ÉæòkPl@#F*‡rÝÿhèClRJÍUÉþâ¹ÄW†êÞ䵫«íJît‡Š„@D@¬#B1˜¨Ä8›YF„$͸ô ´d‘À&óÛÐ?¬ÁÍÀõã',à8UK>׽ͯD±Ù]Vl³{$8ì¦`ì0o%þRùTuU¬cP”y2ˆuø]Y‰qŽûr"-iF"3 ß8Ó ­ÙÁFg`„𲇾¶4TZƒb3^‡rÆ•³$8ܼžá>ÔþÁã'¬±²‰•À®?qoWWÛ•Üé ˆ€XG„b0Qƒq6Á0¶6¶ìOgßÝÅaâ›áÌõ]æXê¶òåõ@´„ø%ÙÃEë‰Û<”îÜ9^žÃ:R/£2ãu¨=“†64Çroæb "Χ]]mWr§;T$"bŠÁD ÆÁnG?~Âï;,3®ƒÒZ&ÛU`¥ÍýƒÊ̶”ª,{è ÙVQ Š–l8‹­^žÛòðÁX4o¼ñMÛéîñÃ_¾‰Vs@ÂÌÛÕÕv%wºCEB " Ö¡LÔ`œ™þÀ•_›±ÀуÆ9ÜÂÁ›”yp¾Â_ t08j=»²fXU¤ÍÌäl±6ºÊátNXGX3 å¶¿F¿?õ©nYWÅ:u@™'ƒ€X‡ß•5盥²·ãÉp~è Ds/VùfŠü*¬•o¿Û¹ÙY•ù®ìaÀvOkPlCWÒÔmiRØH˜(¼ Mé1XÙ2ÛÕÕv%wºCEB " Ö¡LÔ`œmY ËZMB?ôMåwþ²Áp4£ÖcñimVeêË*I‹™5(6¸1x7jwuN‡>ÌšSÓX…þ—ÚÕÕv%ï÷‚r„@±Ž>&iN%ÆæÀ¼¶­„µÅ†©tX·ráhAÎÇV#Ú·fU–=ì@Ýîi%Š 7†£~#!8l†]½[3>NQ¬SÚÕÕv%o÷qä[" Öá£]‰qÆ{?(8p œ:œô,~£…Aà[¥*Ë:H¶UT‰bC3ÐO臂ÖA 0&š®6uè`Þ®®¶+¹Ó*4ÜñÈŠÕîm¢ãl3ÚÆ%˜×Û„å•nbÏúöõ|Ëù0~r7Ä}X,ãT–=tÀi«¨ Å>x>GR Ê1ã›o~ ~b{.Ûßà}:20b-´««íJ>ØwÊÄ::€tN«0Î××(!Fo÷£ñ œÏÕCÖfœÃêÂ÷«ŽéŽCÜ'ezìöïm~ ŠÍÒ*ÔÞkÃ,ž ê:ôa¬ƒIFs:µ¿!p‡XÇVÊ•# ÖáwP Æ 1°¶º0g?ú0?~;jM‚Sçi™‘íÉ!nkA¿Â$Û*ªD±mVerø‚é•ÿûþ×Í6Ê Ãò+wê°]]mWò¶I»b>ò•gö£‡Ø ‹…8›Ÿ Û:œ:Oý[NÇfdl KE!LÇå³øÅÁöeai1³ÅfºÐVÅ2‚aqGÁüú?þK ÒŸx­‚bß»©]]mWòØ5J±Šj0ÎF$ÒÎ~ô‚#Vîœvn6‡<Œ¢`Ïí`D¥S'=•=LÑh:]ƒb N¡îÞ~Æ|X-þÛw~CiÊ4‚KÒø§]]mWòñÞP‰¸C@¬ã‹¡T ÆÙ6ÉÊÜÞˆ•ín,.:™ÃŸƒ;¿11æüêdÄ{¸æ!WöЧ­¢ĘþÃg#@çzQ‡Y[9n Üav0oWWÛ•Üé ˆ€XG„b0Q‰q¶Ág"BçìG5æ0âAâ.ÄAïm†'ÒÔž÷jÝeÈÞaÑxjwņ6Ø:þFÏç0Ù7ô kXbDô˜ëÂJyB rÄ:üÚÝ8Gñb èû÷ý sò·ÁjÒ£‡¦á3æJjOõ÷c„Q C`wÅ6wV¦Øâ¸1Z=6ÁãuàSŠ«·ÀSpªº*n¯‡ô´ëðûwwãlâ1‘þ „ô?¬Cˆ=‘vœÎ9HÀRú-ÇÙÃEë‰J›Å)Œc˜æƒ4>ˆjxwØÀª.ÖѺJþ{‹€X‡ßõûç—ç,-á'V×–™„¨CŸ_ýæŸùõgÁ¥ãßÉð¾®<Äd¨í›<±œ¶ŠöWì^(ö_Çõ5þ¢hààÐuð¡‚ãრC¦²ƒy»ºÚ®äNw¨HDÄ:"ƒ‰Ý3f9Äè8ƒ¸Ìd`Lãúš}fã,IXf8eÚî— n‡Qîü–ÍK>î×'{8¨$-fî®Øó,qÝ ªŽ‚ùú·~€J£«¶8+<‡Ý+[f»ºÚ®äNw¨HDÄ:"ƒ‰Ý³­.Ácxmå`ð,u¹¥ó›Û¹ºp&MnÖÕ^]cðòÓÛú3ìƒhÈÂÒbæîŠÝ ¥MƦp{Fù9l$ÄÒc•í‹íêj»’§]¦´C@¬c ˯Ä83½¢-Žôàô72‘N LÍ\]Py,Ê4•m unXÇ!BõXËÔ—=4­8¿•(6H¦nEcÀ2ñgk²nb§__ûŠM;íêj»’uŸò…@Š€XGŠF?]‰qþÂëo1ÖaâY޾¨1‡†;Ìa`düCå/~êÕ˜4ǰ³8w¼nÖܹ©ûYT‰bgºÁºMŸ¡Ç,³%T/Jë+v»ïîv%¿Ÿ’îz.b>b•g“㺈‚ƒÛ¡&CÐæwg>œ{¤å°Æ£3Æ·a Šˆî`uJE•(6\"Ç­å Žâ2Æ¢Þ!Ç¥Óí¾»Û•ü”ÝËzˆuøØVbœo„ÄÏóêR1èç?v#˜hÿCkFi"±«/{8†Lsù5(ö,·"Èü¦Á²,[Æ…¿´{»ºÚ®äNw¨HDÄ:"ƒ‰Œsìvƒ î^~ãËLºwquÁ v7sé¹ìáRäªû^ ŠïVý: ÐL,,ãrŸ‚vuµ]É«Ór T%b~·ägÆ~÷«ïÆ7 ÿ`ªÚñÛìHÓ°fü.Hø›×CNðµ³&ƒ)…j·Ö;lM{›î`§²‡ƒ°´˜™¯Ø¾÷Kg)v¦[NM8rXšà}tXHÎ쌣®íêj»’·ø Hæíëð1Ï4ÎXBóÉÄÚÁ ?Ðú69æägÛs–‰l\éà66£«"7²À*‡5,»|FŸþæ7#S ¢‚Ìaøzü#{8ŽMc%™ŠÕ5?‘¯Ø@–éVdñ:,ýe¶…„3ÕØ®®¶+ycÏ€ÄÝ ±øãÌȃýøêÿuˆG¾q¶éoää‡!Q5Hp¡~T 8 ùapã@ÉBŠY„1çÙNŽoŸ1ùÙCɶŠr;Ÿi¤5ó; vØæÞŠ…‰»‹¶¿ûÞs Iš‰s»ºÚ®ä™]£j÷±_&³­c ïëÇOlu A>ùfop@HmrLçgl2­áž7€u0¦ÁiÿWž9ö“O"“®þ,Za¢œj„6%ᯎ‘=ôµ¥¡ÒIÅŽŠ:7‘¯ØÀÅ8›m„zó(q:ˆ!{µPC†xßU¡(V¡]]mWò»®QJŒ# Ö1ŽM(™4ÎØX£l…™Úç0&|˜}†'¤ù1=Ë8›÷> Âaà¯Ø^÷rü:"‰‰±&eÇi.R±£¢ÎMÌRl{FÂèÜÁa‰ÓA$iJOQ\fÅèÇE±ÚÕÕv%ì;e b@:§“ÆÙæ5°Š}ãl“ Ü/"g–qF*âo0Á !8à9ÇNY&à_Œ}q0_öp–3'{Pis2g)6Ì}†KpàãÁ á ˜楦y“Ú_\¦Ç(ŠµÐ®®¶+ù`ß)St@ÃyüNå{x:iœ±±X?fR:9μëxeàÔæõ³*ÏêSÙÃYpÕ\yR±;úœšÏ:Ìa)®·bA îЃ Á: 1?ÒøW¬c+e Êëð;hÒ8ãÅõã`ËfÍ8³¤…Ÿf!ÿÑÙ˜¹Î7ÎHHSø¬Ú¯}úÔl²Í­'³ÒùÆaBûÙ›×Ϫï4'!{˜ƒRur{LuýüYŠÍŒa»`ÕD6¡ñ𓩆g3eiWWÛ•|°ï”):ˆutéœfg,'¬0Í ÃC0¡å Ú ã|u5f™‰ÉÆ/DÏÞΪܹ۩SÙÃ)„š)ÏTlŸ` –ÎPìWÑB±ññx5ïæ rbû jþà¼L»ºÚ®äƒ}§L!ÐA —_G“ô4ß8³ºä‡¿|Ób“’4Èif¾qfâÖÁÒB"€…à`/ÏŽN…LÓ³*§_ÌIËæ ÔD|ÅN•6'¯Ø…V‡ˆvG|ƦZÚÕÕv%?¢õÕ{„€X‡ßÙ™ÆG†Ž“ÔNˆ0¬÷,ãlƒÏ\®26m÷2«²ûRÙà ížf*vÍèÔÉUì—ç„c õC¬0‚o"†Í…T¬c.bª/öE@¬ÃÇ?Ç8Å&…DÿÒŽežË:˜Åf†Å™'7¯ŸUÙ¿ýN©XGvOs»¯´99™¬¾as¡ÐG—ç%,¿šùë˜ ˜ª ëð;`Ò8Dzc±IÇ u¦qiîéÏÕ…ÍU&Æ)Çt;I ±ŽŒ¶““Š=¦·“ù™ŠMt4îAø/K:7©»ÿ`t±Ž>&Ê5# Öá÷Τq¶(aPŽNä󸢶H¼fp 7¾¨i)Ž„Qâ #fÜ"/1±Ž£Hú]'-Öá€ÓVѤbO²‹± ™¬Ãà¢2ÑÎ-m.IsÝÌÄ:ÚRþ“ÆÙX#}›l¬„ûEädgÌò …°½*D‚AŒ1±m§{œ@‰áˆV¾ô>ìûÆ€ N ŒlÔGg4†>Æ?bãØ4V2©ØƒJ›“™©Ø†s+öAÕï¢÷Oà·XG„B !Ðb~7Mgl¬±‹ÎŠ ´HQgæ%ÚíLãL*³ H³á4;601Cfáî¨LæÀmbØ£m¿º`µ¯ÝÈ@ÍÛ,±Ž[$šÿ?©ØQQç&2» ºmt›µZ¯T`ðò;Ý÷=‘ØiÎüJåÃI»ºÚ®äý^PŽè# ÖÑÇ$Í™4Îl5k/kþb*™Î€o0ga™¼úÇŒö,ãÌÀ"&ØØN÷VŠéæÒ YðX‡S™‘mÆ´™¬aá’³Žë=…)"{¡h=1©Øcz;™?K±aŒÌ9–šÒØN÷‘®Äú$ÚÕÕv%OñWZŒ! Ö1†Œåççà8j~GŽHBx‰ t`ºgg.Á„ˆ‰ê>~âˆÍ0µÉÀUà0ŸÁÊfÉAzü„I–±¥.éweS4šNç(ö$Á¬0K±1dÌ¿\^¿ÏpŸ}p=Eu¿n™íêj»’;Ý¡"!ëˆP &23#¼Ü‰‹Î_Èyõ;”c.ë@¶hoiù&:úÈÔdÈ1þí?Þ§ò èUfʾASƒ?Ñ=„¥ÅÌLÅä~æJ¬Ã|žq{¶$¨µƒ|»ºÚ®äNw¨HDÄ:"ƒ‰Iã µ XŒ…nÑÓ¿c&z ãÖŽìá2ÍåO*ö˜ÞNæ¯¡Øæƒ„ÏsdÅL žªç³ž²æž& < ±®IãÌ:Æ~#ñ€„àãÁœ¿vŒYé5Œ³s/ƒ¬Ã2ÿôÁÇÈ7+I°q¬ÙÃ1dšËŸTì1½Ì?^±Ñ@¨{ )¾I¶ ‘Ï4Ò¯´««íJžâ¯´C@¬c ËÏ1θvÄX^ ðsÌÙô-íãó ‘»ÁÊÁ¼–Ùò¿VÇŒ-x±feÇàm.?G±£®ÎJ¥Øì<{§;+ª˜䙚r»ºÚ®ä³:H•ï-b~×çgF6XÃÂë›îÁÜçGçƒÐƒDbìvÆ*™“AaÐGg_üÔi®FƒåÞæç+ö,ÊAåYŠ:0£¢<8ƒ=B›¥• ·ëoPÛî»»]ÉûN™B ƒ€XGÎé\ã ÷`¸À¸opÂpíYƹ#•މY•íw%?-9‚_Ç«#Û¦d;€´{:W±ÇÔ¸ŸŸ£Ø!DÿaµO óz¤É± ¾AHÖ7ŽAûÊÖB»ºÚ®äƒ}§L!ÐA@¬£HçtÒ8?}úvÁ«›¶3‹þöͲåäçŽ0Ó±XÐjvšY¹3²ÝiJö°H»§“Š=¦·“ù9Š gìÂ6™%a ž—AH¿ûÞs¢ÐP‡ú|+3L÷†ã-àì+G²‡õh÷tR±'ÙÅX…ÖaÚ:”è&Jæ‘À¯EE“a&¾âT¶NiWWÛ•¼ÝÇA’o‰€X‡ö¤q6WRâ|tð8Æ8#›ïšµÓ=Ì!®~%<é˜%·§“În,p~eÙC_[*Tì1½ÌÏaƒ@93†a†å°,¾Áœ ŸPÙÝ ]]mWòÁnU¦è ÖѤs:iœm†…íN&­q§B¦q¾YcÑ?X¢;¾Ó=½AQ 7D‚å—Ï&‰¿ùýˆ+)[kqˆutàTO'»£®ù§™ŠÝÖ!°zÜ¥ˆ¦›~Çîb…vßÝíJÁWB8ˆu8àP4iœm†…÷u¾Y¶š™Æ™_vXW*Oîtó3„ÁÛ5)p‰°2ed®ÜnÞ*wˆuÜa1”š4ÎÆ:˜‰{½Y"ý rš˜eœa\ÈÛ¼Þ¡¤kR,Çc‡Ø¤¬»±–Å:†Tà4ó&;ÕÕYéYŠÍp®¡¨(cncD‚ëÏ©oÇä ^»ïîv%?ÍGEwU±ÑIãl¬ƒi~õæŸþ³Ø³Œó¬îÓ;ò‰„E ‹ñ—ø¥éÇ~”=L±m:=©Øcz;™?K±Ã‚¯ÃÆÇ0d›ìÜÐü:lìÎ&¨8]Ю®¶+¹Ó*±ŽÅ`bÒ8ë08øwÌJÏ2Ω)Æ{ÿf§ûA‰_Í„WàãújÞÝ#D ËoMöð»ÆS“Š=¦·“ù³›G†ÙÃÒ2Þ}ha88ÁL8ø b˸S‡íêj»’÷;N9B €XG“4gÒ8ëÀ¯ƒáˆ5Ö°˜0aAë!Xt*k û“ÝVY˜wþþ/8â²Ùô‹4ÃÚ4Å16¾ëËF(ZOL*ö$»«Ã:Xc…¾!>Eq¨ ]åÔ–¨tà¥MË„lœŸÿ! ÓhD;=eÞ×é‰! Öáwè¤q6ÖÁ@ñ˜ËÏ1ÎQ6,-ËB¼<™Pöž‹§1¬÷a™!f™„™÷XÚI`óÓÊñЩf§²‡ƒ°´˜9©Øcz;™Ÿ£Ø4‚ÖÁypPl#½$8Ó¶E†c›;‡‡t–´t¾Ø®®¶+y§ t*ë„%fNç8Ö1i;rŒs¢2>:»áWj2YuŽÆIƒƒáªÅ8`±AKP!­Ì·8Mgs:õe;€´{:©ØuÍ?ÍQì0™’.·%ᇽ›!EQMùQfæët2ÛÕÕv%ïtN…À bƒ°ÄÌIãÌÞìL¯ä›e«™cœ£æzÁ¤vßq÷°qŒ.ë¸ 1”¨À´}7Ä@ß¼ÞJÙéÞ*Û–÷ƒëVAö0öHë‰IÅž«Ï±~Žb£¥ÆŠÓ¿Œ]8Œ‚R\y§`úÝÑ®®¶+y¿”#úˆuô1Is&3S„+gäaìo´ÆDŽqŽ’à¡_S'aû‰Gg8”b„±À‘0P“ñj‹g΢„±ñ 3ÔFQ⬊ _3c¯ÛIÈvi÷tR±;êš:K±3DEÃôbòA½ÃC1þiWWÛ•|¼7T"îë¸Ãb(5iœ·YÃÇ7Œo IzÍ{j˜bÛËVï’Ã1Xß2­eø§L¯Ò©,{è€ÓVѤbçÓŒNÍ5XkXPNt;ßÿò'}ÅnWWÛ•¼­G@Òî…€X‡ü¤q¾c67Í[>™¤Æ0vlr²‡8mM*vTÔ¹‰LÅf. _ö»ºÏCÑßN3C¼ŽÇOÐOþâàdJ+Ö‘B¤´h±¿§&sº†{‹%´=Yð÷ Í1f´3³‰g~]QYÏÒ7ÚÝJ㿆¾;æwj­ŠuôÐm5cR±Çôv2?S±¡Ç<6‚ýà€l0‚AsˆéÔ!Ã:h– A¹PsIrÐoWWÛ•Üé ˆ€XG„b01iœÓ5,áÙ-ëÀϳ ëÀ‡`·á Ã–²/Ï™Cá0(ödæÈmƒÖ2—ÐN÷“ÐF…IÅždc2Y02°†/l1Ëí’Fçp1í|h“}ˆØeÀ*³ÆÂêvª¥§í¾»Û•<Å_i!0†€XÇ2–?iœÇÆ:ʲ–!iDhÛ¶»†%ÞÌí8F»¸MÇB¼20à4H³v„}âñ=Òj´ìavO'{ŒTLæç³CŽý°£Ï7¬|ŽÍxˆU³X½ƒ»ºÚ®äNw¨HDÄ:"ƒ‰Iã¼ ë`Ö†él3¹$8e´y,JCÐ ÜþÁV3jÝ¿µhêiÄÜ9hœyˆG¿rÌ‘=ŒP´ž˜TìIv1Va.ëÈA’ùJóå°eÝø8Ù©óÝvuµ]ÉîP‘ˆˆuD(“Æ9aa˜·6!50È…Ç:þ!&!±8ìWëMý=lfF¤¾%c‹a ¡BZ:bÝ.-{8¨$-fN*ö©˜Ì_ƒu,@¸]]mWòݤ¯ÜCÄ:üNŸ4ΰ‹4J/zÛãž_g°~‘YéYÆnðö¾a¢’0ÖÁxÅ` ¤0bÑ%Ä:üÞœ4Ḭ̂D‡ÌÁĘ•žeœÖôà 2ætçßËX)­ÙA<ÆFƪ‘/{è€ÓVѤbéídþ,Å^´vuµ]É×ëMµ|Jˆuø½9iœÍ¯ƒñ‡›ÃÆ 8µÄ£³1+=×8çïGïßÑX)í3ÏryýþXòepÚ*šTì1½ÌŸ«Ø+áÖ®®¶+ùJ]©fO ±¿C'sð=ì•i¬‚GžüµcÌJÏ2Îð;V“øxøÂ甲à…pèæ+Â%¼Ävd#­'&{Lo'óg)öz0¶««íJ¾^oªåSB@¬ÃïÍãŒkGØ öì‚I^Ü8x”5Î4ËÄ î©ln ¥ Fáðïe ô*ć¤AÚçoX9ûàaA6ò‘=¦½ìÅžÔáÁ bGjƒž²#Ô×+G@¬Ãï |ãÌÈ+VxksðÿÞßþ¥Ï=ò³Æõ‚Ű ­à Ê%œýèý;JK­eÛàžfY6KËéBÚ´2iÙÃ ížæ+ö µp2ó{UôÚÕÕv%_µCÕøÉ Öáwå\ã ÷`D¸{¿²(uÌ>çg þÌÄ ëøð=ÚgÑ«/yN©µlÓ7øuÀ”Þù¢ì¡N[Es{Lûùùн*bíêj»’¯Ú¡jüdëð»rÒ8?}úvÁa;Ëó— ªŒuØß¾Y¶œYÆ2À%Bá»0PF‹5:FÌ•”Xèx{c_œÅ:z`a:쯉Mܰ®üý½Þú9ÝïÜÃ=œì¡ŽŠÆÀ}ˆ@„± ÊOÐS–¢¡ôé! Öá÷é$ë°~ʱ‹±ü¹¬£/§ù‚¦ù,€eþ%äÀ4ì¸|ƤOZ§“fålØ—öðÁuD;ÝwðÑéñˆuÌÂP¬c\ªÜb~—M²›aaõ1v1–¿ë`g’èaBWrœ{¤”x×/Ïá$üʲ‡’*C@¬c ™Á|=eƒ°(ódëð»2“u0€'b,‘þ$+±¼>¾þ¯üÅM”Dˆ£î²B ÃLp%…8Ùfµ ²‡8*C@¬c ™Á|=eƒ°(ódëð»2“uðâf`!]É)™k°Úä`n…f[H„Y˜GgÎ=†¨¤ÎpçÀ]äßþã}Ÿ¢È:Hªh ±Ž1dóõ” ¢̓A@¬ÃïÊLÖÁËzìØ’uØz®‚´?:ƒEØ•Ñ{<Ä&%ƈUëJG Ö1 <±ŽYp©rsˆuø]–É:˜› VùªkXúr†qŒWgOp2aÔ……-„ÿJ—ºô¿k9%ŒªvÊW˜”«L¾ì¡ŽŠÆëCf0_OÙ ,Ê<Ä:ü®Ìd¶0plXc0‹ÊŽ«FåàÆ Áýƒ >±Tö0B¡D>bùXQSOÙ,¸T¹9Ä:ü.Ëdþ–èhšÒ£XÇ!œ¡34ñáßæèÜÏ;9‹OeCwŸ¿(Ö1«÷õ”Í‚K•›C@¬Ãï²IÖÁð+L¯¤Œ"Mƒ0»Ð¦9–žÇ:.ŸE9¨øuàŠͰƒ­Ul ûøÝ#²‡Gx?¿.Ö1«ßõ”Í‚K•›C@¬Ãï²IÖѧZ ™Å:ðÖÀt›¨$:îñB¼6U9¸’ïÖÃ+c•ã·ò²‡ùX©fD@¬#B‘“ÐS–ƒ’ê´‹€X‡ßwû²F*p epæ@Q¢x1ˆá _X`vâ}±†ªs³Ö]9ëß~§Tö°ˆNsëÈA)ÖÑS¡Pâ$ëð»õxÖÁº’Åc_xý-Æ+þôÁÇ8ˆÞŒ`ÜŽc ŠÍ0 ä„I–0Êñà¡yt°s\©ìa)$ïU;b³º[OÙ,¸T¹9Ä:ü.;žuÐÂà —œ–tÇ7.ìp¢jXDt–¥°$Ö¢’²¯Jx^è#{XÈûÕŒXǬþÖS6 .Un±¿ËаŽÅcƒ²Ÿÿ¡ŒÁ¢À:?‰Ëf‰8jÏ+/È”=\š¾"Ö1Kô”Í‚K•›C@¬ÃﲺX‡m#{}=¶=‘7:E0†>ü{Ì/•=ÌÇJ5#bŠœ„ž²”T§]Ä:ü¾ËaÄöì¬[IOi¡ÈX[ØÛÞ.¬Ãe‹X_ì•JeWö´›ë˜Õ¿zÊfÁ¥ÊÍ! ÖáwÙ$ëxúô) FØm-eišaÄôHs,ã×eƒràJ`à­ñè oX´eBöpK´OæZb³ºROÙ,¸T¹9Ä:ü.›d,Q Žfô™Fš3‹up‰ïüü÷&*†S–Óú’¯Q*{¸ª'ߦXǬ.ÖS6 .Un±¿Ë2Y«TbØsK¤S²ÓY¬GŽÏ‘šñî{!ÁçâÏÏ9e•ŠnùWöpK´OæZb³ºROÙ,¸T¹9Ä:ü.ËdÄÅé‚©s½HÿFšÑIä°½Ò2ÄVC›×ÄE¿|Æô C+¾Ø+•Ê®ìi7+Ö1«õ”Í‚K•›C@¬Ãï²LÖÁàÃØÑ!ñ4‡uP/ŽOâµõë¸I¿ºÁ½ Ke ‚yšë˜Õ×zÊfÁ¥ÊÍ! ÖáwY&ë`ð àðöì‘ft9¬ƒu¯¬„í,…øb¯T*{¸°§Ý¬XǬþÕS6 .Un±¿Ë2YÇ`ôÑÍèœæ°Ž1ÙÆ‚œ³Í=ûÌÆo±! N§ìäsŽLÈ àýüºXǬ~×S6 .Un±¿Ë2YÇÚkX|!c©EDÇñ#:–àÂÔO¬pdBöðHïç×Å:fõ»ž²Yp©rsˆuø]6É:~òÓ³GÓ+¡ŒÉÓYc Y„A –´Ö°·Ò¡«kͽ„ø!¤™‹aÆG¬c*åm‡€XÇ,¬Å:fÁ¥ÊÍ! ÖáwÙ$ë ^ã Dñ𤠳XÇ÷þö/!\…MX¸\XØ2ôa·8œ@ˆ$öWýUê°ÿ,µ¾þ¯«ùÈñ¾5‚ÒÚä}»ñe÷«§lnúV+ˆuø=•Ã:lxe³¼å;ÔÂ9Å:`\…A —s¼I_ž3*X k^l–CÄÿ63Ke3RµˆÀG}åà€{ÄL%ô”9à¨èëð;q’u ïw#üŧÂß–%òY¬ƒ †,p"eU ¦NŶøˆñú·~¢Š½xlÁÏ”=ôñQi›^a"²_¤œAô” ¢̓A@¬ÃïÊIÖ‹ ipŸxüĸoy f#Íè$f±_ÈX¼I¡@ŸÖ#H+™=<À{øu9uÌít=esSý¶ëðû+‡uøá/ßd%‹ôo‡lÄÓ5X¤xw0*bQ>BpÇOü{Ì/•=ÌÇêž×d>ŦT:cè?9÷ÿöõ”ùø¨´uÄ:üÌaL©àGÁÇ Ùxt†>ö7ÒŒNb ÖqýòÜÖ¹„›:8x|ÿËŸÔX‡ßÅ*]Ô›ÃZfENפ ƒƒ‡Þª>àÂÇÇG¥­#€†ãîÕú]¬'ÿ$ë`§û”oàð9¸¯}‡rpºë`†þúZüZ Ùa³-b멇ZC€iG† kDÖwGÌû¢òÅ:¤§€X‡ß¿“¬ÃvºçÍÎàF&ß0²ë0aðèøoÿó)ÖïSþ=æ—ÊæcuÏkÚÒSôç@Ò{ŽsûzÊpTtˆuø˜Ã:x­ãMÑÍðsÖcŒx0ÏÂïJ¬½wsKeç"vŸë£‡¦ü…›ƒc ÷“œ{×S–ƒ’ê´‹€X‡ßw“¬j_®¤ü<ƸǬ'R†\˜XaÒ‡#Øü« óú÷˜_*{˜•jÆà`è!ÏÏÓ+¸:ëã# §ÌÇG¥­# Öá÷à$ëàGœ½âÇþnÉ:ì^àÄô`†y¤’_‡ßÅ*]å0ÖÁß0 §ÏbS©¼mÄ:üþËa¼Öc{ÖawdÑÑÃЇâuø}¬ÒÕÀךñ ø ž…Õ.u: ‹uœN_êN†ëBå./“u©ƒ àˆf!;X3k§äìÀ:^<ÿð¿ã 0)ÜCû°Üu§RÛ"`¾ð ãÛ^¼Õ«‰u´Ús’;±§LÖa¿ã $[à[cd#ægúuäw‘THöŸ}çïÿ"ÍIÓ¬™ ³*D(=ÄiNñ(ô‘=,ä=j=7ÖÁ¸Ç=ºí#nUOÙàé« ÖáwÒ2Ö³Gdc‰LÖA5ŽTH~?ŽMš@Q(b€Å“2Ê÷ PXúõcÒ²‡Ç w?¿k; »ã~‚0ë®õ”Í‚K•›C@¬Ãï²IÖÁ/8^ô¼Ü-(i2ÇÈFÌÏd8à1vÁàF<¾àƒbcÛ S–…ˆè2<’f.NË.†îÞ~Ñwt˜ó½E#çÆõ”å ¤:í" Öá÷Ý$ëà§œÍeðr¿9Ejá$òYGØÍí+¿H1ÖWIg:ÆFJýÌ”=ôµE¥ƒÀœ¦c™ÁL=eƒ°(ódëð»r’uÀ+–©²çìáÈŒPšÏ::¿j²Ñ-N¤q7–·ô0ÖQ(H‚졯-U•ÒYÌô­t i<™¬12+7Z­`×ë)+¦šª±¿S°üLs†,(²aüµ£+lì»™¬ƒµ0©úàã± ç´i~¤¶Ý'ßâ´óõ´©¹iÙùˆíXŸi¾IÕSÎÉüLíÝñö·¼tÙç¢lk[â k Ä:|”&YÆâƒYþ˜Ï´Û7ÃW©œcöÙ÷üeûûàVzy]lcqÙô*O‹ulÖAeŸ‹²­m‚.$2ëðšd ßytD׎$q$ë0ñð e} ›Ér02äœù”Ÿ}ûK×/ž[M¿²ュ²‡ƒ°Ô™)Ö±Y¿”}.ʶ¶ºÈD@¬Ã*—u<~ÂzU‚ƒÁ@péü›úwÖ’)Â:hÊFNØOÖœWÅfšÔ‰•Cý‘/ƒ-ø™²‡>>U•ŠulÖeŸ‹²­m‚.$2ëðÊd,Y…]ØKŸàϤÙòµ ë }Ö°XްöÑ٠دë0fXà<$œÊƒ-ø™²‡>>U•ŠulÖeŸ‹²­m‚.$2ëðšÅ:‚3烇ÅYÇ}ü!ͦ«_ß}ïù Ø8‘Zó>P «ƒë`噲‡ @Ûë+b›!_ö¹(ÛÚf èBB ±¨LÖÁ¬ÊzcøŽÂ:pØøðoï‹¿L  Š Óxý¿‡•³Œ@~HØÊÙÁÊ 2e€¶×W*bÄä/–/<ë–}.ʶ戭"!° b>왬ÃfXVë@BE‚;Çã'ÐHÈ Ø6Åc5CðÛÝX+/È”=\Ú^_©‡u@€ãÈÛ^h¬zݲÏEÙÖV½q5. Öáƒ6É:ðß zNŒuÌŽ¿ŽCÐ0ÒƒGæÊÙ(‹aíø·ÿx,^ñÊ8Xl+ãÚCHläÈ„ìá‘nùõzXª.Ö‘ßõzÊò±RÍëð{m’u 2 Ëôc4Íb~ÖÃÞ~b8ŽÛŒþËnt‰ËìÎ:PZÛ<ˆ ABæZú$§ZÊ>e[+¡JjC”D@¬ÃGóÖáŠf±Žïýí_ÞüZ|5\XGxbztöaÁkß©¶øTöp1tÛqwÖÁ’+åjc²Ïüe¤n{(Ö¾bÙç¢lkkß»ÚsëðÛu<ëKüTÄ‘ƒ¿xŠrÀ"Bİ¡L†#-q6mI«e¦e3ª¡Úî¬#‚€Nj†%¢1™ÐS6 ‘*4€X‡ß}»³n|ý_/ˆÂÁA‚Xdü`tÖ°PdCÙö—(¦c®§þ–ÊÂRgf=¬CÞ¤³4DOÙ,¸T¹9Ä:ü.Ûu˜xüZ$‡¥møbp+~Q2šÍ˜vzˆuø]|ª¥õ°˜3³-'ü)Ëʶv°ëÖE@¬Ãï¸JXGúkŸˆÄàæõý¦ÑÅ:ü.>ÕÒzXÇ©"ï«,O(ÛZR !P b~GTÂ:Ò_‹0f[ÅfSûξöï_¾`Ff°ò‚LÙàíõ±ŽÍ/û\”mm3t!!‰€X‡T%¬ãFH°\]À@ˆÂ1ü±Ýf;eî²—N]ÿTöÐǧªR±ŽÍº£ìsQ¶µÍ@Ð…„@&b>Pµ°Ž« ³àjkXàƒbã×aÑ?|çï8XêBDtr+/È”=\Ú^_ëØ ù²ÏEÙÖ6A™ˆuø@UÂ:X›?xhqÎ ß1(6{Ά:2«¢ˆèƒÝŸL±ŽÍúº,O(ÛÚf èBB ±¨X‡í9ËÄ D\'¯ ³/¹Å?§”­â‰Ç©¼Iû(Ý“±ŽÍ:º,O(ÛÚf èBB ±¨XGŒôë`Û†W¤ßßÝ~Ç:ÜÁ$‹ÞU8.%{x~›~»UÖ12{X»Y-Ï©\ö¹(ÛZÜÔ„(Š€X‡g¬ã*Ð („±vºçtpå,~,žµaBŠQÍÜ<ü{Ì/•=ÌÇj÷š±Þà—ÏÂ^BûÝã¼ôâÃ÷@5&Ái)$gµ<«rÙç¢lk¥ÐS;B hø`¼©Rí·ÞN ¬ I …à`G9gå,¬ƒ!ŽÏŸ}²AµOâ5Ûì~p:fA×È.m¯¯ÔÂ:.Ÿ±õjÉ›/Ÿù€U¦&jÌ9uêã/ËŽÖ– =æhM#³ZžU¹ìsQ¶5= ]ëða¯„u0²ÁØ>¢Äxü›úw\;Å&XÇ^‹:cp`d°?SöÐǧªÒJXG`Ÿ (- Ì©‡ÒU Cé“RpaztæÔ‡ÉÇé™y´Ä¨ªÏjyVåëë²ÏEÙÖôT$vA@¬Ã‡½Ö„d˜šjwº™Alæ_:w¼; }d ¹E35°ÆQ¡‘$›o³M  Bð§>¦>c(­?ü囃5ÉŒþN¯}æsŒò}p˜ˆÖ›Õò¬ÊHRö¹(ÛÚzÊ{! Öá#_ ëxyþý/Ò†©Ã0òÈÛËoÉh±I¯3>R}v¶ìálÈöûB=¬ãòúfw{X1Ú8ì} Ta‘øã'7K¿Çµ—¸»´‡a2ͧNɼm©û?¿e¾9«rÙç¢lk]t.öF@¬ÃïJX”SÌÈ3¦.ØÐà¡—|°½7»m¦ûå#©rTRöð(ø¶ýr ¬ƒ;F]£;qfPK(õÏÏÿÀÃx4Õg2ÅǃBÎãÀéØ Ë¼–çˆxeŸ‹²­¡§|!°b>ò•°Ìi¦ÆsG;Ä&çÒT~X˜~ýÙ ¬x)>÷*] ë8ŒZ¿ð¡¸'9½fI™ß)\…¾=Võ†“pàœ:?qŸÕò¬ÊˆW–'”mm =å ½ë𑯅u<:ûݯ¾k¢òƒâAzŒuàÏov›‘8÷íßff©ìa&P5T«„uðç“,W£û%xA! v„‘=wläš5‡ „«øŸY-Ϫ\ö¹(Ûš‰J…Àöˆuø˜WÂ:Ü€HàegÃÈs.v̸u¼ûÞs3Ú°Žÿ<ÿÕü{Ì/•=ÌÇj÷š•°ÈJk$¦Cvà/}uÁ„•9:£yTy(ÐðÀ=XËáúZ‡Òì–gU.û\”m­ƒ˜N…Àîˆuø]P ë€9|ú+¿H†”ãœK¼Û‡Sl{1^G¬pdBöðH·üz%¬Ã|9PÝsæÑÙØB5Ë`í[(9Êìp DÉí ¦ßø¬–gU.û\”m-«„¨±¿#*aXì0€|uá‡t£#ÃæSgéÎxˆ¿~©ì¡OU¥5°e[O%B`Ä:|Ük`6Þ‹œüî›ØéÞ¿™£Ke†p»*a w°zÀAbl$Å…:Œ6p¿ŽÇOÒ¢NÚªÙ_|<¸D§Bç4¿e¾˜_¹ìsQ¶µ:»# ÖáwA¬ã0ìÌè4c°g§{ÿ^Ž/•=<ÃÍZ¨„uÄ77>µç,ÕpÿAw©ùò|¥àÂÌv>W<#¼ÎédËiýüÊeŸ‹²­¥w¤´¨±¿j`HhÃÔŒTÿößÀ=üÉnÿŽŽ)•=<½¿[ ë`Š× H‚í äϘˆ.Ÿ܃jøutbþ†<< 8;¥GXlëΰä´|×GbÄÊeŸ‹²­E!•• ÖáwD%¬ƒ_|þb™å„0Ö îò‘=Üöe­„uÀ‘½+”ƒYœ—8PxˆGÿöÑ‹]|?<¤ 6ŸíWŽ99-/«\ö¹(ÛZ¼#%„@%ˆuøQ ëè.•ͦöïkY©ìá2ÜvùV%¬ƒ! Ü9¢ëlÁAÃÖÌÂ7¬Ží"Ä|ÊØWà$Ñ=õëÿø/\kpF†¯ÏjyVe/û\”mm :å ½ë𑯄u¦#Ú^L"ÖÕ{¥RÙÕ€]£ÙX‡9BGÕ…<øc$7g`È"ΪàÔÙ¼žR6¨µb¤½»ä¬–gUæeŸ‹²­ÝA ”¨±¿*aXW¦­mÛKàßΑ¥²‡G¸å×k`áõý茩|ÕEÕ#Ç®îîo”†Ø5/ÏögßþßåŠcŸy-Ï£ìsQ¶µ14”/öB@¬ÃG¾ÖÁ¸46“Ék6…틽R©ìáJÀ®Ñl ¬ƒûŠ>™ªka:ØQˆïB*ˆ›á€óô׿¥CÁgãÁC¿ò¬–gU.û\”mÍAOEB`Ä:|Ø+aü ƒl09Î'†ïð%_£Töp TWj³Ö‘Þøx¤9¯¤ÓcŠÁž¿R39ÕàÚÁ½A’ÂÛ䬖gU>\¡ìsQ¶µ[ô_Ô‚€X‡ß5°‹M篃‹;ììßÑ1¥²‡Ç ·ñw+aL|¼ý£oLÞ;Qh˜X 1:’'9þÉi|V˳*G¹Ê>e[‹B*!*A@¬ÃïˆXÞtŒG[ÄF­œõ{M¥ °;ë€3p|þ쳌Ô¬ÃóÁèwPÁqضžé;Ø¿ž4¿_™œüÆgµ<«r,>›1ç˜DÙÖŽ‘Dßk Öá£Zë@BF÷b)>²‡)•§wg_xý-£ á/58]0±‚†ssK£ö|eçüÆgµ<«r¬ìsQ¶µ(¤B Ä:üލ„uøBnV*{¸ÔÇ_hwÖÁ Ç×¾öU\CIØ ± –~ÝM&¦Keã*ÚÎWæ6žß2šU™úeŸ‹²­upÓ©Ø4Ü1»‹·»biȦhTžÞu>ðãoZzr ! Ï…·†MÇà⇦Éo|V˳*skeŸ‹²­U®¥ï" ÖáwºXGŠìaŠFåéJX!Cã‚ÖÃÝCHYäfdò"¨Ïj|V˳*—}.ʶV¹–J¼{ˆ€X‡ßéb)>²‡)•§+a Älˆ}&Ö·ÞÊ\Œ™›×ç7>«åüÊeŸ‹²­Ý‚ªÿB Ä:üžëHñ‘=LѨ<]ëëVXûâùØÉÈXë0ÓÏgyË] ö~ñmŽßòm­›ÿ~å²ÏEÙÖ:7¢S!°;b~ˆu¤øÈ¦hTž®…u$[Æã¤kÇnÉΆ‡±5/Ö¾©ÄZ|&ù° æW¿ùç$ã69§å@“n?“b”}.ʶv{ú/jA@¬Ãï ±ŽÙÃÊÓ•°b•‡þŸ0½BbpçúI˜ Ñw-g2 /¬¾rÇ=.Ÿqú“Ÿþ8mÓÒ³ZžU¹ìsQ¶µ>Êû" Öáã/Ö‘â#{˜¢QyºÖÁ(Ä÷ ›À,Þ]$)€Ì•°­¡-Y}þÖ—Þ{÷Gä°†…¯§Õ:i˜ ‘=‚ççã'ðÛ‘¯¤¬cV˳*GaÊ>e[‹B*!*A@¬ÃﱎÙÃÊÓ5°Ž›ÁŠ« cF*Y‡EýúÓÛÀHæ\8®Ù6åÅó°:æ°="Å2–îÉ2«åY•£”}.ʶ…TBT‚€X‡ßb)>²‡)•§k`8a˜þ .:¾¿ûÕw9Äš¬áWŽ[¹c”2¾Á(c_ÿ× óùìx~ÎjyVåx#eŸ‹²­E!•• Öáw„XGŠìaŠFåéXYTsè¬îÁ`B>n“ÒlÊ7 ¶œ65)FÙç¢lké(-j@@¬ÃﱎÙÃÊÓ•°f@ðë` §|0.¯o#Á’·Yõ™[‰Û% Â%Ø:¶•Y-Ϫ\ö¹(ÛZå}ëðñëHñ‘=LѨ<] ë“ Büë#˜³êÛÒZæ°K¬ƒÒ5, [ž#vÙç¢lkñö•• Öáw„XGŠìaŠFåéJX¾õ×_e†‚ÃÙ¹>‚9«>KcXób“à”v ƒ¬cV˳*—}.ʶUBT‚€X‡ßm±öVÝAXöÐ×–ªJk`ý¥²ï¾÷JÑbsë3¬±I!Äá€ÏøËrgµ<«rYžP¶5m• íëØó•®(Ö±°6[ ë½øËBNâëOîtOP¯@<?á`b¥¬¡ü–S±'Å(ËʶÖA9B`_Ä:öÅ¿àÕÅ: ‚yMUÂ:.þü<Ývpú£‹65+mY;I$BÛÊÝvúçsZ!;n[öÅ(ËʶÖÇ@9B`_Ä:öÅ¿àÕÅ: ‚yMUÂ:Ø!_£€ç«AºF¾|FÀ ¦Kì¤ÅùÔQ¹,O(ÛšžŠ„À.ˆuìûëXÕvÛÜu°u,Ì¿ þB$8ð¾`3YRªÙpLøâ§<ïÓJ*—å e[óÑV©Ø±Ží1_éŠb+Ûh³»³¢s`$ðÏ„EÀ@œÙ Ö¹ÀRð£°=jIpŠbvA%•‘­,O(ÛÚ tÊ;" Ö±#øe/-ÖQÏÖ[Ûu€¬gM·¥mEªR†fPÁX‡ÅÍà[ƒ}QIed+Ëʶ62…ÀŽˆuì~ÙK‹u”ųõÖ*að‡¿Žëë°Ó½»s=à 1Ãpâ‹2ÃéXGTRñÊò„²­¡§|!°b{!_üºbÅ!mºÁJXó,qÃV³->ª5rB万‚£’ÊeyBÙÖ|´U*¶G@¬c{ÌWº¢XÇJÀ6Úl%¬#E’€«Fø\]0 ‘¥iêàÂ[ê߬´¯_Cå²<¡lk)°J 먡ŠÈ ÖQÆ“i¤BÖ±EWý©–XÓ³êo_¹,O(ÛZI ÝëØ½ J ÖQ ÉÓhG¬#§‹P”²<¡lk9 ¨ŽØ±Ž-Ñ^õZb«ÂÛ\ãb9]&Ö‘ƒ’ꂈusߦÄ:öÅ¿¶«‹uäôˆXGJª# " ÖQÌ}›ëØÿÚ®.Ö‘Ó#b9(©Ž(ˆ€XGA0÷mJ¬c_ük»ºXGNˆuä ¤:B  bÁÜ·)±Ž}ñ¯íêb9="Ö‘ƒ’ꂈusߦÄ:öÅ¿¶«WÊ:;ÏV”ˆYˆùõ_¼º#-Ó__ç6î·<$ÜXËXÑ¡ê óʶ¶P}M¬†€XÇjÐnݰXÇÖˆ×}½ZXÇ Tì×öÿÃÛC6T›SŸ½äÞþÑ7>|çï8HpêõIFËìüBSFÈtbšQ¹,O(ÛZç^t*vG@¬c÷.(%€XG)$O£JXLÍ4HIL›UŸÖÂñèìÁã'᯻ÉKNË„aï72)vYžP¶µÓPfÝÅ)! Öq2½)Öq2]YäFöeŒ|ÿËŸ´McŸ¿õ%öq³áˆþ;Ýnvn}ûDâoþé߉šÎAhôOå}èfµl¬ƒ¯¤ò‰m—+ËʶÖD9B`_Ä:öÅ¿àÕÅ: ‚yMíË:صÁ‡?}ðñûŸOo"Üሹõé ‚`Ö&vÖž/¦cbVËÿöïC]ú‡XGÄS !p$bGXÏ×Å:êé‹$Ù—uàxi·Ù@„ýå>öúž[„ù Äæõÿþ§ †V_Ðr§ûŒÞt2ÓÓ²£e[KåTZÔ€€XG ½PD±Ž"0žL#û²ŽAëxt6X4˜9YŸMiã¸DTqý:ÒKL¶<«rYžP¶µôF”5 ÖQC/‘A¬£Œ'ÓH]¬ƒeª‡•ªÏ>ÌxnýÃÐ>Ó­Ïjùê×FK|±Ëò„²­M¢B`[Ä:¶Å{Å«‰u¬nƒM×Â:®.˜þ`„ƒIë—çXΪ ¸vØ$ >«^ã3[Žb“ðÅ.Ëʶæ¢2!°b{ ¾Ê5Å:VµÙF+a?ûö—˜Uùßßý $&ãu̪Ok?ùé¿÷·IḚ̈°¤ÅFT;mVËÖ&Ó7HN㜶i™eyBÙÖ±U$vA@¬cØ×¸¨XǨ¶Ûf%¬_‹ïüü÷ãÓ_ÿ–Ó>úÈA5¿¾-ËýÕoþ™¯à§Áz<cÏm§kŠßårc-—å e[“YùB`/Ä:öB¾øuÅ:ŠCÚtƒ;³æ>^<ËL<|÷½›¸åþñmNß¿|1ìÜú·+g{Ð Ó+4λŸ¥-Ça“Ëë°ôÆá3eyBÙÖºhè\ì€XÇÞ=PâúØÕƒþüÙg stÓâ!³Dëj£Iöeè¡Íw “xt„8ç—Ïðëxí3ŸDsn}a5.dÀÌý‹ŸzÀµú/hÙÈÒï~õ]kˆauôùÌíÅÊò„²­ÝʨÿB Ä:j鉥rðrùÚ׾ʷùEöÿÏÿ²4§o¼ñÍ7ßüÖÒVõ½æØuð¦þô'^Ã)‚ƒÄMzdq+Ü`V}ë|En¢‚ìñ“ÀszŸe-wŽ¥1VÃi¯á»Œ²<¡lkwR*%ê@@¬£Ž~X.óÚüijá_˜†±¦ÎÉÄÑny»úfãìË:`µiç#ñ:æÖ¿éœ« Æ%ø.Gðëxü¤ßiËZ¦Y£4°7É•Êò„²­%b*)ª@@¬£Šn8Fˆ« ¯Ú€rë€oD*rLÛún»ìË:Æpc·”Y¿>kX,":4â=¹@&½´ß25<¼øóó074µÚ·,O(ÛZzËJ 먡Ž”™æVp~‹cœ’>²Y}½iþöÞ§Õ’ä:÷®šÜKŸ/äOàAAóBc<öàŽæd FOä BÐ VOÚjP »-Ôº XRw[º”zÔÒiƒZ}n•êÖ=u±×>Qy23þdFdfìÜÏ&Ù'vdäÊÈ'W®|Ί+Údu!e„pâFàØ|ó³šü' !6NáèGøS—'Ô•îµömëØ÷ªg½ºú«Ëøµ±,¤ý¬z ;1aλàÛìÞÝÏb¯o žÐþæšÁð‡ãD†oìÎMüâ…ã0"“AˆGäö×å u¥Eº­]B`Ä:6½úI͹a¬×¬£ú)$ð´h„u¸t[÷î“¡ nl®ƒ8Œùí™k’YjÖ¦÷³NFO‘/™ñ$Ã7LŽ[´…9¹Åm{òëò„ºÒz]ÕO!°9b›ß‚*à>Â4,®¢Ù+UP=i!°Ž?þó¯òÊf¹y›KB9Žê¤ö¸ Ü™ÃWˆ Ï—l3gý¨ $§9\L]žPWZmíë# Ö±>æKœÑR„A!äÃ×ëøü“ù_¦-ó.ß°Õf¿ûe²‚@l"ÇÖ}@êJ‹t[»„À&ˆu”þ{ó.3X®$ëKؽZ©­GÙ``îç“n“bç8æ{ðÐÅu„?uºÒ½Ö!° bå¸ïÞ¼Ë –+Éúv¯–@ŠgÃèÑ0ŠsXL¸ÍŸeæ,ô#rë> u¥Eº­]B`Ä:Êaß½y—,W’õ%ì^-ó!=®ûv÷€ødØÞBuÐȧîRWZ¤ÛÚ%6A@¬£öÝ›w™Ár%Y_ÂæjIfo96æ†0×5ÅÔö!9Ãzf”»´¥,wËê·‡í :lz[C{Žr¿ÂA¤·m_Ô}@êJóTA4‚€XGùØÜ¼—_B\‚Ì`Ÿ6÷n®–L¿bë‚ãgt+}yj{ĉá\bà¼0б‹!ûñê+¯Ù¢·بkû‚ðQš±—oK´S‚¥Œ6¶ÊºH]i‘nk—رŽrØ77ïå—— 3ǧͽ›«%¾^ܼ²ýFê­Ð» §¶ç—ÅëÞýþã—ÉTcŒbô^À:4%<ƒï7¾ý –Üc´1»ˆ¡¥küׄ‹³„¶>TÖ}@êJ ÷Z{„À6ˆu”ã¾¹y÷—€ùýð½¯ûŸµ 2ƒµ\SÎæj ‹ óÕ¿û·îg“Ú&Ä_zý]æ°P­ÃBOp¤à™0 ‰óºDDiW±ûåÓw„n_ݤ®´PŸU/¶B@¬£ùÍÍ»¿&b`ýÏZ™ÁZH®)gsµ´w}÷’™ig¼ëóÛÓÒVÃwA´'~ ÜÝÃ}±t†£´a£À®x€h÷Q2JÉR÷©+̓ ‚h±Žò±¹ywãÚDÊ=¿ÂT:»mQsϯÊ/Í$È ÖBrM9›«%clÝKþìÉ3F+º5ÝòÔöÝcãe¬‡íàèp“Yò>?7™ ¤îRWZÞ媕X±Žr¬77ïö¿˜Èa]]áâ’‘îòK3 2ƒµ\SÎæj‰sÀùzs@z?;ˆLmß94QÄÇb^Ž;þ ÈyÞ‚;%ò©û€Ô•é¶v Më(‡½óÎÿe,Ç7~Þ1°e)3X†ß6Go®–vÙDºi&‡ùª˜‡cjû¸4ÛK¼“y„ðÜnLxÁõ‘s¬kssMÞõHãºH]i‘nk—رŽrØ1ï\Hw0ºüº¼™AÅ QKäyÝ0å8†SÛÇ¥Ù^FXà6 Ö¾‰)÷òã‡câá(œ¢îRWZ>j#ÖD@¬£íFÌ;òÍýOrùõ$È ö9‰Ÿ¨å/¿ÀLœol U„¦™xH§¶÷F 8:˜±âÎzðgä+ßýõ(ë` 92¶Ò½ËÑ‘—R¬îRWZí› ÖQ{#æ½üBBdCÈ´\ß‚ZÚë›™§¨>ESLmïÅÆ >q^Ò†À4ðr¸pënÎËð(¼…´q΋K6ü3¶Rxݤ®4ßI„@#ˆu”߈Ì{ùUD$È FÀivW jɤÞׄUàC`À‚ïx4ÅÔö™à¿óη8/,w‡¥ü")s¾†‡›C†˜(sμtÑD†ê> u¥ ¯Q5B`[Ä:ÊñoÁ¼—_ED‚Ì`œfw5¢–n,¿Áƒ‡¯ý៰Å€9µ}þn„åâ Á‡4vŠd\«—¬¸… B ±Žr 1ïå’ ÖB¦åúvÔ’½m¤çŠäëð`Nmï \¼ÓÝÌ!.Æ#š›´+  yÑ»5½rݤ®´^WõSlŽ€XGù-hǼ—_˨™ÁQX¯lD-'-¤™íYÍ6>›5ywà6‰ÏÍ5‘¥nÚïØXL÷غH]iÝ~ª,Z@@¬£ü.4bÞË/$$Af0„LËõ¨å¤%ãÁ3³½4¹wŸù­Ž{¤XÁŒÛd =8›‹E ¬fk’ë> u¥Í¸v"E@¬£ÞFÌ{ù…„$È †i¹~sµœºdü¤ödþgˆ„u娀0?%N ¦Þ)¦º0dÁ¥ÜÌ—ð§îRWZ¸×Ú#¶A@¬£÷ÍÍ»]‚û§¯“nºÐÿÜ…Ef°‹Æ©”7WKb'˜ê—Œ'†Ó愲æNjo³K¸dâõ’q}T¹;OŸ>ÅÅa‹Ä!?}Ò°á)ê> u¥ {«!°-båøonÞí0Œx§ù§í_þú÷ø¹4#@Qø¶önð:ü‘ cÓîžFÔ§„_2Þ&ÆòN –ÙžÂç}Áä\¿GDrþ.[ÚþåÀÍó+„w³ŽôDÕ}@êJëuU?…Àæˆu”ß‚FÌ;†‘ÿ%ñ9³Qˆ³þ+üþ¾KÁPÛ ]?I™Á 'ñ³µìfé'4®–›ÙrµÆ¡ÇXÓaü*ñ5ÚòïÞdòtØRÎö˜¬ö€èqË¿SjyŠˆu”ߵ̻ýér8“wñ°Ò=³C—f úþ¿ó 9WûW.Ô+Õ×E µäŠ7±D”a ¸Ä/3³=þ´nÀœVä‡FmâçŠì¥'bħ‹4®ËêJ‹t[»„À&ˆu”ÃÞ‚ywÿÝ®±e9œ]à}àcd¼¦ \ÅýÚ‰ é'3Øä$~fª%þþ—÷k¢ác´‚J²ˆG6\ ñ’>D7×´çUžëŽHµ'ÿ|²Ý?ÑèïçWnH±ûI-#Ëã™ÁÂF·#ORë> u¥u/Ze!Ðbåw!Ó¼GlxdW®y¿¹†9VgËÜc'7 ]šñ ‹ý`âñùñÿ@eCH¶\Ÿ£–¾÷u÷¿üa~h÷•ˆ\µ<Dìd†Büõmxæ·Ï Obš-ÚNðÚnLÉÀ'zëWã@~Fîuݤ®´H·µKl‚€XG9ì9æ=Â+â»2Í»°ØH7ƒÝ¶E.ÿà\ìǃ‡¶E#‘Œ Ù쮤Z2Äv¤‡Œå„A>>Hk¿F43S-‡·IçÌý"¤‹6?×u;:;¦îRWZÿÂô[l€XGùHšwoÒg2Í{÷*pãëÀ›Ñ­–ifÞfdzCË Ñk¿&©–08ßCµ4ÖÁ}LµÓíçGŸfé«û€Ô•貪…Àf áÓ¢Â6ëi»'Nš÷éΩÏ4ï Ã˜5#ÑŒÔÛ+ƒÑ–dAê\Çï7ÇFË FÀivWR-uŒŽ¤Tc‡ gh-ç‚u°Ò=’#óMà 4Èl? Oâ,ã·ãÉl†K ?8=\ ê³Ç‘P “LT-&9ß;.ùP[÷©+-Òmí› ÖQ{Ҽ簋P›\Öqˆ&e¶,æ‘oseG²)Ò G:Fž¹˜úˆß[f°\IÖ—TKíÀë•q Ó@FUxëð•CåÌUËGíB B`¼÷ãP˜6f¶Ï OòcILÕáêøFÿ9E¤'GÈ<Ä1Âo\÷©+-rÚ%6A@¬£ö¤yíüšLónÞæcêæ·p'vr|^á͵M³½ºúØ®ÝwýÉ=Ld{€œÄϤZºˆˆÛØQn±é¤½aQHiDK3Õ P6ãd†±Üqôf´‡9s1¡ð$Wˆ¦~3Rï‰qr¾éùËacÌë> u¥Å¯Q{…Àú áa)„=iÞ#Ö;¹+Ó¼Û\Ó¼/Ø Ãè?•øŠÍ±Œ}æ?\¦1ââ à‚åÆÌ©!#3X¨!›ž£–¸ÅŒxÐø%븸Dyâš™©–Ç GµX6¬`}|òÛ÷dV†…“ô?O>Àãѯ ÿ†{ðÁTŽU÷©+-|MÚ#¶A@¬£÷ó·á‘½ùæ{‹Ó°¹~£«dr"h cÜ쥀 e£ÀÁAf0N³»2ÕòÑÏ~Ž+€oSÂn¹ŠZÂ71bDƒ ÅKÃ5¥=Ú#B 8Ũ|¢˜ än×aÑ‹ñˆëü¨ó(wÕ}@êJöV5B`[Ä:ÊñÏ4ïÙ•Ï:ÌÉŸëÇŒw2û‚ó6‹u”ëAc2Õ’@iÞC¦;§>Ó¼›=äÛº×ûù²Ïr<{Œ}ƶû$Ï–1r<¹ÇáHoØ_ÊQ©y’ji¬꡼p߇»¬&S- š¹ JÂ6©= ¿”-ܵ<Ìá…ö„#‘:ž3æN©û€Ô•¹í› €†GþÉݤK'wÒ¤y™îœúLónD‚q ç°¼èÃ;Ëcùæš7¾n–ÁbÃã_™Bfðät’'Õ’ðÈ's¨{> 7úVÏײ1¼Âú–7¢ß“Úw“Ÿ|@P~6zÂ|ü¹Ÿ›k›xr§Ô}@êJ˽Fµk! ÖQŽtҼ簋P›LÖÁUØ›âw¾eeþg^g¡Ùqñn‚@^ymý…¼‡½RÍ$Õ’gßtâÓ`ƒXÐõÑNRŸ¯–üîD ¬tŸËôüÊyosXðrØl—øÚCPŸ ÄEŒfÇØ«ËêJ[Bµ$S” ÖQ‚ž›4ïëÜ•oÞ1§l6a–ÿ— ?øº-¡G÷Û-Ú2æ7ö‡Ë z(N¨£–uñè_\ö =EÍTK 7BÁð«kä4-n4©}~,“#'ÚT»ƒprG$p¹ûa´‘y.&™Übæ ´¥ï6¼ó«îRWÚŽê‡h±Žò›cÞ{F;ÿgÒ¼óÙÈm7uìÒºÓo®ýÜÆÑCdGai¼2S-Ýû÷à…pÄã°Þ®Ÿª4¤¨Iµ4p|ø%¬C%gñÑGC'µï/õ~v…ã·é¥±HÔaOlÔɦÀ†9?P¾Ž. * Ä: À;šiÞC<^Ÿ4ï6ã?vwsÿÜEí$g’Cò?›E“Fp눀Óì®Ij Íð[\!moR- Ü è!AÆ:LÓF½p3ÚgÆ2!™¹Àn"ydzaî‹®÷Ã:€‡ŒÅox ¹h¬Áèwݤ®´Ñ«Rlˆ€XG9ø“Ì{ŽIï¶IšwòØœDÌ»ûïì¯ÃF®#—Fcbêü§(2ƒ$›Ý•TK\Ëúîêa¯œTK‹e `Ðc¸Àï-LjïtþÞýx,g¡·4ƒiûP BY’=ñÝS\‡‡B!PŽ€XG9†Ió޳ؓ~æ›wì*ÿ”ÙåðÏ¡‘K{ý-IHŠ0š%פëˆ Ù쮤Zó„^Ü(Ãp‹(j¾Zš*Bƒq i^EC¸Mjö²!™…¡ïÂÎÂI­¥Ÿ·ÉŸÃÂQ´u˜úºH]i‘nk—رŽrØ“æ=b½“»2Í» j»ù}–»à°pgäÒø_ÏR;6ÒXf0N³»’jÙcÎ?F€Gg‹(g¦Z:p:ãîg`1z·Ë>™íI»1ü„„ÓxØ>ÔØ‹=,PKpéȱ¾Í¡P÷©+ínOõKl€XGù=Hš÷ˆõNîÊ7ð瘟#póÃqF3|*iþ•ãÁAf0N³»’jɇÓ›ÖzX¼•’o+D”3_-»SP#‹Ñ{sÚÓ14Žm›-dOŽ*½œnáÍ7¿ÁŒ`[K ‘ÆÇŸ|Àø£ùë¸GøS÷©+-ÜkíÛ ÖQŽ{Ò¼G¬wrW¾yǽì=ɶºJèÒpbó®é7j"ŸnËH3íj Lµ$ˆÔ¢€Œ²ã‘ÔIä«%b‰ä´PgKG)§=zîÈn²ò> ‡u¸–Ö>ÕØ$¸Ä­r"†oÀÇñ™ž¦s¦ºH]inª(š@ f°l¢g§Ó‰LóžċmòÍ{0ìîUúŸ j»Að›ëœ»/3èq;¡Â$µd8Ï=}c.öP»5ùji.8„S°SÄ1ÌiIF{y>,'D™ •æØQá\»è¶-zKc:3ÚØ*‘ì— °hÒá4[xݤ®4ßI„@#ˆu”߈Iæ½kºsÊùæýÎ…Üæp¾Sy÷ÿ~âÍf£ù?Žƒdï"w¿’jÉØ…M`±‚­ÌbüýÌWK¼ßÍ™–Œ ÙüöLH¡«L?ÞCÆè½a`Å2Òàô šÅµy~Å…ûì7øR|FdøñÖ}@êJ^£j„À¶ˆu”ãŸ4ïëÜ•oÞ»âɹ~0 ç£6·óÅ¥ËfþÈ †±iwOR-{ѤF6ºßåÌTKKòé#ˆ,ûV²Ií¯ãâÏLrø²Á ÍhL´ž+ö„K¦ž¼"0Œ±Qàç°±¯©û€Ô•æ;©‚h±Žò‘4ïëÜ•4ï–ÃgÖ’ÿérr8›mgÌšü Ä ºQòhžj™Ár%Y_BR-Isѱ2,G”3©–v½Œß¡Z]ý‰üMmê2‹•øð‹ë¸Meol¼ÈòÎ\W£(þžv/ÐWÎ.Ô•6»:P,„ž3²¿ÐÙ÷!6iÞ#Ö;¹+iÞgäpöŽn%åä©– u¥uº©¢h±ŽòÛ4ï!ÓS?ϼãÍð©GG/Ð<Ìjø -ã£í2ƒ£6^™TKc6OÖOfé®ÉÑÏyjY1 Ob<òŒ³ …Ñù­Äu@­_°Î2ùHP4ŽôÇ‚H„bV ¨Ìè‰4®û€Ô•é¶v Më(‡=iÞ#Ö;¹+ß¼c'_N€M®tÏec„í¿93Åa dÃØ´»'©–+ÌaYóWX*'2"ß~ìJý²õVˆ³\@4€««¡ì×pʯû€Ô•6ì­j„À¶ˆu”ãŸ4ïIjiÏ:0Œ¸š“³ízí_9þ‰ã¿9üÒlÎøÈ €iº:©–/YS6lõÝBµ\#ã–ï}Æiãc|Nšø„Œoþè—x‰Â…B!î9¬û€Ô•6†‡ê„À–ˆu”£Ÿ4ïëÜ5‰uøéØÉØ¿r‡i¶–5ý;?~[kΖë@ƒ’ji¬ƒêIŠ>ºE”3_-E†I+NÕog¹º¸Ž±+,Óáîç'¹¸ìÖ ËŒBÚdžm¬¦.O¨+-ÔgÕ ­ë(G>iÞ#Ö;¹+×¼ˆDwz`h!×ëC阿À¿Ô@Q"¨eË•d} Iµtù:îÝ?é9, js]ùÆýÅ/Çpön<–iTN¨²îRWZ¨Ïª[! ÖQŽ|Ò¼'©E¤A&ë0"á&&ÇPKèÒ0ѼnÇ#íþ|~%ÖÂêtë“jé}õ íÊTËÅÑ»frß¼Û¨ ¡l¸š!¡KsÁÿ¤j|å5<Ï4sÿ$š™Á’-×'Õ’QÆ×؈íÝ"ú™¯–‹BÔ]g]h„‡»,ä)3ðiR·ë> u¥Mº5+ ÖQrÒ¼G¬wrW¾y'zŸVØâ+ÝsÉVÇ!´ÔÌÙrhPBR-aá]Ù"Ê™¯–‹"gΉ fÁYZ²Y:ôxúô®ËêJ›q9:D,Š€XG9¼Ió±ÞÉ]“Í;>çƒÛ¹ÙaërÀ%!¤Z¾œÃrx#Û{¹ûQÎÉj™Óãémàt/_’H@Åqèá´ü¥Lƒû÷&õ¥.O¨+mÒ…¨±X±Žr“æ)ÿx°wertàsÆ´º ŽÞwï:Ÿ_1Òí,ð¡Ðõ¾ÛXfð.§ñ+©–GÖñà!®&–òúf> ¾¯/½þ®qöYŽ(‡åèð1ÒÃÛC\›yö[ä!WXrfÊPN¨¦îRWZ¨Ïª[! ÖQŽ|Žyø±ÙUżÃ4°¨î}ÁDÂÔâõ^ÚpjJÛQšÃR® MIÈQK´Åæ°ù@™èbTÑ©Äm¶üf¶àë°Ü¤WWìn€ÀdX®Žlpl6uŽ™WúÔå u¥UºD‰Õë(‡2Ó¼›%wß–‹©“‘)dÛ©Ï4ïf‰ëàÅA” NJüÅ ö2—~Bç“‹¶È –+Éú2ÕÒÖaAÍP‰Óbn”äÞ}¯œ6yvgÇ©xÜlÜ}ßn‘t££rB•¾¡“êëJ›tj5+ €†GþÉ]¡;8E¦yÇB²YEÌ .>5S9ëðîe^ü7ªœ‹ÊQxÍ\³«›¯#Ô˜f2ƒ£06^™©–§ëëØuNü’=hÐYجàV}†£“ÚαuºÒ:ÝTQ4€XGùmHšwŒcånèÜþÛ:ÐF7À Ÿ3ëmU`S¯ÇêÂI8/ÿáòÒÁíÌÏÈÿ}2ƒåJ²¾„¤ZZ\‡±ŽSôuäCÊ•¢íîß+ÈÃacá{ÀQ ¾÷u»]· áQml•uºÒ"ÝÖ.!° bå°'Í»'„•Â@˜©êö,£Þ:b!lœ$'tiŽuœÌ,6aÃ=ô'Ô˜z™Á8ÍîJª%>1B1¹õè§i&ÊIÙE>ÔÃëí°9ð×8¸äClÃåÈ“2Ú=7sï¾oIcCMBݤ®´ÑkT¥Ø±Žrð“æÝŒ¶Ída™*ï]0ç!¸ï‹Ë¡U÷5ùæ.Áô@Þ#øœ™‰€óÙ.ÍMT|ÌÕLµ%îàØÈGf0N³»2ÕÒkš/ ¨¶ùša!_-[À"Ása“gaYÇ„6aÖc§ ßLÈ7¶««û€Ô•Öþêƒè" ÖÑEc^9iÞñÙ2’¿WöO“1 Ì 6 …ýƒ94ìVShÞ}¼Ç¼K³£dKÐÛêØ¤ZÒäyûîì?ýNR_¨–+cë Ã ”0œd“Ê#Ï-]ã›k†<¹\ãÀì˜%=n+«‡N·2bå€'Í{7S°i ³aÓp}°-gÞ#Ö5ÿÂeó±j§å$µ|éyó.¸{÷—SË•Q‚u°áý³dbÌÞ²i\£Ý°Æ\»Ea1à O` u¥Eº­]B`ÐðŠEߢäI“æ½ûO%æúÓÞcÖaË}zK>ZÈ7ïD‰…Àz›\F6yQÝ2ƒ]4N¥œTKa‰3ê$•ùjy*pù~×Á³Éÿø ÉñK™'‹gÖ7ê> u¥ {«!°-båø'Í»óuØñaŠ"#æî?),Û+¯…¬º¯Ošwâí]Îó›k:Ûøä6 ü,¿4“ 3X É5å$ÕÒÈpõõÚè Iµ\óJëžË±Ž½‡XkÒQ^¯ÌWËÆQªÒ½ºH]iU.PB„@EÄ:ÊÁLšwþsÇט@ˆÌà(’W&Õ2‡]„Úì˜u˜g&oß6<¹ÝuºÒ"ÝÖ.!° bå°'Í;¡FpAxŸ6£*X6«÷•C#Ÿ4ïÿùÛß½ñ×½ÍÖ½43ª87^a´%ç#3˜ƒRkm’j9T¶üš¤Z¶†F~ÈãgO%ÿ,P&ÊF!#ê> u¥Eº­]B`Ä:ÊaOšwH…p³í¬aÆèŽˆµŸmÞá!£—† ímO¥Ì`™–ë“jѺä®ÙjÙ2bÖ7B¤ äðvâl±Ö/^Ä™ºH]ií®žh¸Öœ-¼é9æÝýuHCJã—¬#cÁRæDaÄ ¬9Fz€ ÓtuŽZ&ÙE¨ÁRj٢ϯ˜TNv‚£Ž!O+NòÒãÖˆ¨ ! ÖQl¦yg8æ°øÁÊÔ„¬º¯ŸfÞo“Žº9³·åÑ dpçû?øžÙÕã’šáö2ƒ£6^™©–^Ó&¦©eãHÝí Ü9ýèÓÇL*‡¬9ÉKÛÝ¢_{C@¬£üŽ&Í;!Dt0a$ô1øùæùØFl& Bà\ÒÀNÂàŽ÷9,VKd…Ñi¶&@f0dÓÕIµŒh]rW¾Z6ÑXç\à“åÕ¹]¾™Eœy@ÆÚëê> u¥Eº­]B`Ä:ÊaOšw àÄp…¶ˆ‘Ï7ï(BœÑ Î;ƒ`°×¸—Qá'ÃÙ¡Ì`™–ë“jѺä®|µl¢Ñ¾1ÊÆãÀÜ.[ÄÙ­àüàáhc«¬û€Ô•é¶v Më(‡=iÞÝ2[ÝÕµ ÝŸFŒ|¾yÇT2V‚…ÄwA!¸úÛ 7uš3„oRµ3‘–À¹2ƒpšÝ•TˈÖ%wå«e³ø;Ö]ñàaOJ°}ípk=n¨µkˆu”ßÄóŽÿÁ":,wFÃn5ö²ó“Ì»÷WøBèꌢxò³æ°u¨Kª¯‹@ŽZ†´.Y?I-ë^×ÒÒðLïÔÍ¡ç°,»äŸ bå·z’yÇ\cÁŒu$m; –3ï0œl.®cEr9à’ƒÀ$µÌQÅn›åÔ2çÒmc‹âËÃxF(DNZ×;QWZ¤ÛÚ%6A@¬£öIæ½ÖÑ xsýù/þ>‚ƒÌ`œfwMRË.£È)ïžuàñ`ó¼`É[\÷©+-Ùy5+# ÖQxÒ¼û9,L3!/:¾Le?«%bç2ï ©°B¥l,<±¦¹pIÈA ©–­KîZH-s®ké6‘’£ƒi,<#lŽuÜ\ÛRG¡S×å u¥…ú¬z!°båÈ'Í»ùly³‡¶ˆ‘_ȼӿ̱Žr5hMBR-#Z—ܵZ¶ƒ!܃|¤ ¬ðhÀ=Ö|@Ä:ÚQõd Ä:ÊQMš÷þ–»Xˆ©ˆù…Ì»- CÚgLkdÑGf°\IÖ—TˈÖ%w-¤–ë£?#±OLžu®E“Æ‘Ò^!€XG6TÁ†9æÝæ°„¾#F~!óÎlY?µÖÒw/¯öÄÀȉ´«"9jQ¼ø®…Ô²âå—Šzö˜`'6“Â=´K)ž:^Ü" Öq‹Äü¿IóNN ¦ü3/´E,üæÝhÆÕÕÇvÍŸ=y¶fˆþ| uä’jѺä®%ÔrÊÅ-ÛÖÒüº ¥l÷î» ’x„?uu¥…{­=B`Ä:ÊqOš÷ur“æ_ÿ»aN»ÆšÈ§Û2ÒL»šB ©–Iji°cÖa©zñZbR¼<,‘õ¸éuºÒšÒIuFØóÊ›-|2Hšwø|\£…õÍ;Árd!p+Ä…}ó—/3è¡8¡BR-#Z—ܵcÖÁ,³/½þn÷F»Œè,ðù§ÝÊn¹îRWZ·Ÿ* @ÃÅ: oDŽy÷ØsKFj ÎZ9bäsÌ;s`ÙzWOÁÙ`{›ÆB!Î=d{ØžÄϵŒ(^|WŽZžJÃN²žSÝ»õ6"Éw·²[®û€Ô•Öí§ÊB Ä:ÊïBÒ¼2Ñ›öŠ÷…îψ…Ï1ï´aë^H2@ÔÖ¸'8ßR¸ÐáÌ`›v÷$Õ2¢uÉ]9jÙ.4Ñžqi<¤‘z*N\–óu„G!ë> u¥E¯U;…À áòuâž4ïKçë nÄå2"Ýè“l³±éÐuÙj³,Åb ðº`T© µ— !Ór}R-“Ô"Ò`߬ÃâH}¸5?ßøöO"÷ºîRWZ¤ÛÚ%6A@¬£ö¤y§¯uÛðß’+ƒXµîVhÞýÊ,á7Nº´žÇ¸÷sx”Ìà“ök’jѺ䮳[àþøt¸Í„•>y^¡Iݤ®´öU=<7Ä:ÊïxŽyDZàâHÜÿ-Só’†Ýä˜w|,UoÖÒï-þ-tin˽û?üÇ/[ƒ•È¡^©¾.9j™©„Ãf9jY÷rV“ÆãàŽçÑeD_ h÷ˆu”ßâ|óŽÃsâ:ZÚó^MŽyÇyB³î…8÷ÅÅe·¦W6þcă2ë5èþÔ?_]4N¥œ¯–=•Ëù™£–§T¯Ÿ6ÏŸ¡­ÃbÎCž‘^³îϺH]iÝ~ª,Z@@¬£ü.L2ïLZa^žçäˆsóŽ—ƒ­÷Áéù…€ÍÚPˆDÊ!Df0‚d³»&©eÓè¶ÉQËf‘‰w ožCžS6ž,ç9ŒrøºH]iñ‹Õ^!°>bå˜'Íû07©­ u¥Eº­]B`Ä:ÊaOš÷¥ç°Ø%Àa˜Kò 6\(£>a<-Ô[ÿjì‘‘ôPœP!©–“hF¯ñŽYLƒÑOb9ø×€©a,ð)rëë> u¥Eº­]B`Ä:ÊaOšw@f¡ܶ$Çýî™ôîÏ|óî<'—–‚ÃVè^ÚþöwÇýá2ƒŠ*$Õ²«fSËùjyBˆYW-®ƒGé˜Føv5–È…Ô}@êJ‹t[»„À& áÊ×Qˆ|æýæÖÁ´¿¢5|yX¸ÓøÆÅx Ûø™AÅ šPËÂë¶«L7c#Ò‰%l#´®~»äoݤ®´‘îªJlŠ€XG9üIó>Œëè->ùO3óŸJ˹Რß\“ï‹ø PB—fÉ$æVÒ¼¹Ž7FˆÌ`É–ë“jѺä®LµlŸŠ}«û€Ô•Vñ2%JTA@¬£Ƥy_'®ÃFU,Ñ:ªx3"—Æ@ hFûdc™Á’ÍîJªe’ZDì˜uÀÆ{ë°@ã#¨û€Ô•Ö¬~ªcg‹€XGù­Oš÷#ë8]›”×=ßãA™­ŠyÇÌ c%l_ùî¯ãsýhÌ´ÙÌÆ2ƒåJ²¾„¤ZF´.¹kǬƒKcëÞ/ó vkzåºH]i½®ê§Ø±Žò[4ïÆ:‰Ç˜[¬±ñ”mFI-ÖÑ»x¾ŽIe{pÄϤZ&©E¤ÁŽYO(Þ??ˉIyH#7½îRWZ¤ÛÚ%6A@¬£ö¤yï²Ì5ÌXÇ÷ð=ʵX‰F ɽœ›kÑ]u1áÌ`›v÷$Õ2B*’»öÍ:˜½bS½ü·XG»Š®žbåw,iÞu0ª‚1_‚uR€8R|)©R°ÍEІ?üûç¡=1„ðÏ$$Ô\¬#„LËõIµLR‹Hƒ³.­{gõ›ÏÄ:º€¨,Jë(AÏŽMš÷¥}.źeè$ˆ¬We+ÝÛø•$U¯CPˆu„i¹>©–R‘ܵcÖÁ¢ö½uí?{ò f¹×uºÒ"ÝÖ.!° bå°'Í; `LÁ˜cа`¬õ@ù8Ârq1ò9æÝr бV&¨"ÊÄbÑqÆ=¸|†WhYŠEf°\IÖ—TˈÖ%wå¨åú—\猶ÚlOVØHúH]i½ëÐO!°9bå· Ç¼ãXµä~ös¶Ñ]V™oÞ‰Á©b—óù'FˆXØk‰Ð ù‹ß¿#Šà 3§Ù]9jQ¼ø®|µlŸPÇ|Ft©„“óŒ0Á<ÔžúºH]i‘nk—رŽrØ1ïxQÈ©h—ã6<Œ\-]~msqáÌ`›v÷4¢–íèC89ÛñQFôPªó똇[÷¨FÌ{wˆ„|ÝìèÝÞú²Åà÷pqQŠ"ÖáA;¡B#jyBˆYW-ÿ9¬ƒIa„dó˜(šôän¢:Ü2båw§óÎd²g£¸.¯ýçæg²ÿ5,ˆu 1i¿¦µl«;=äѸ¹†´CÅñºGãðóN›»?ê> u¥Ýí©~ íë(¿˜wÒ8›gØÍg¹w?ž»szý_ (e³ 2ƒpšÝÕˆZ6‹O¨cÄuµÂsA’÷ò(ÙÂ÷¡öÔ×}@êJ‹t[»„À&ˆu”ÃÞ‚yÇ ‘øËwÞ'.”ðT—lŠáÀ{³¥¶h ß®qø#3ƦÝ=-¨e»è„{f¹IÉ~ÃsA„ö«¯¼fÓÒ‡ŒW÷©+m´Ãª" ÖQ~ æÝ/dB.$"IÁa!s“ã¢-båzИ„Ô²1H²ºCT6 p˜{ÞÛx¦BŸº<¡®´PŸU/¶B@¬£ùÌ»eúbÍú#ëøüSXG(_—ÌlY#víž±„  !Ór} jÙ2>¡¾1òȘcoïšOzÜzàëçÎë(¿¡˜wÆVàŒ³„ðûÈ´£WWÛµ¯œz±pIÈA µÌéjSmaá9²döMˆÔšCbMéƒ:S±ŽrH1ïÌøs.. ¿‡øÜà tã/—]ãñsx·åPšjÚD µlœH¯`.ƒT6–©Ã +Aêq‹ÜíÚbå7±óy ÿÆ“Ÿñÿ@Q’m¼™AÅ ÚQË®Ú V˜Í3¥|§uÕÛÆë(¿Aí˜wÂ,[Ï–Î×1å²Å:¦ ÕJÛvÔ²Dòúa¬ƒDè V2…0•Ÿô¸åÝ(µ:UÄ:Êï\#æä`Œ>3ÈâÆYîÝÇlriðÈŒ¿Ìk—̪©f¨eS˜ätÆXÇGŸ>fz¬¯¸ŽèÔFä Ö‘ƒR¼M#æÃh±Œ°×˜nSIpi¼ÿɽbIˆlЈZ6ˆL¼K6©G‡M<÷ù:"GÕ}@êJ‹t[»„À&ˆu”ÃÞˆy‡`ðš]ÿ¯A<(‹u”ßß•ЈZžzÄ;Y¦ ±¶2qPîssÍðåðŠêò„ºÒ†½UرŽrü1ïdSü§?½÷âÉ8„ùOUï^u Wx2ƒ…nrx#j¹Éµ/tÒP€Gݤ®´… X!0±ŽÙÐù1ï0 æúy‡°Íûó©À|ogdg€¶ù!¨åæ8Tì€XGE0%êlë(¿õ˜w2 ©W°Ïä|t _¡XG1„hD-7¸òÅN)Ö±´|Fˆu”ßìFÌ;ãÑŒ­ä®t?å²Å:¦ ÕJÛFÔ²8jôC¬£Š’qîˆu”k@#æÝ¢8äpc+©•î']µXÇ$¸i܈Z6‚F•nˆuTQBαŽrhļ×a+ݳh&Sh!‘•î']µXÇ$¸i܈Z6‚F•nˆuTQBαŽrhÁ¼›=d%s¥ûIW-Ö1 ®F· –@Q«bµ”œsF@¬£üî·`ÞY7çFþJ÷“®Z¬c\4nA-bR7Xæ¾·Ò=Ù}¾D<ÈV:”V÷©+mØ[Õmë(Ç¿óîWºÿù/Þ¯t?õ’e§"ÖBûFÔ²(&õák_;æõ…lØFRt7^þÔ}@êJ ÷Z{„À6ˆu”ãÞˆygžlæJ÷S/Yfp*b-´oD-[€bRH¯Gˆë¾ùí/~ÿžXÇ$ ÕXD눀“¹«ón+ÝÓm·Šý͈+8óŠzÍÄ:z€œÄÏvÔò$àòd5RߨÒoöý•ïþZ¬Ã㣂(D@¬£@oļ“©Ã//K€‡ËŽ^é#ÖQ ÈUÅ4¢–«^s“±úR(ˆÔ·©û€Ô•æ;©‚h±Žòшyçß1·ºýó+¸…øg“®Zfp\4nD-A#¿ø «ÐnÜb÷ñ§©îRWš¿„@#ˆu”߈FÌ;AŒG³Ëkø'lq;9éªe'ÁÕHãFÔ²4æuãýôž)¶øÓT÷©+mÞ…ë(!°båØ6bÞ?|ïëd%eþScÙ¸œtÕ2ƒ“àj¤q#jÙ“»AXÔó«>}ì<"ê> u¥Eº­]B`Ä:Êao¼ß\C3ýìçv9bå·õÔ%4¡–§ "ëe^f²1Í®ËêJ;Í; ^ﱎò»Û‚y·,a8„írpwŒ¦3šw±2ƒópÛö¨Ôr[æaxÆ+ÍËñêßý?I€‘V÷©+-Òmí› ÖQ{ æ9³˜GVº/¿œ¡™Á!&í×´ –í£4ì!k p¯PŠ âá'ˆ ©û€Ô•6ì­j„À¶ˆu”ã¿{ó.3X®$ëKؽZ.)‘ØßÿÁ÷zÂaÌŸíUúŸuºÒ|'U ÖQ~#voÞeË•d} »WË… ýÒëïËA(©—O\¬ãéÓ§¾¦W¨û€Ô•Öëª~ Íë(¿»7ï2ƒåJ²¾„Ý«åB²²sÁ%ýÅ?üa¥PÒ“FNW÷©+-Òmí› ÖQûîÍ»Ì`¹’¬/a÷j¹¤L?wA¤²YXiü\uºÒâ=×^!°>bå˜ïÞ¼Ë –+Éúv¯–ËBúì1«Û³‘˜4>m–nÔ}@êJ[%IÓ똎YÿˆÝ›w™Áþ-?…ß»WËån.éÅ¥ÛîÝwKuÂ<†ç­û€Ô•6ì­j„À¶ˆu”ã¿{ó.3X®$ëKؽZ.©­aô—ï¼Ï8 Y;àÿò׿9]ݤ®´H·µKl‚€XG9ì»7ï2ƒåJ²¾„Ý«åB~ñò Lcé ' ÖqîVvËuºÒºýTY´€€XGù]ؽy—,W’õ%ì^-‚”|>ǯÂVºW¾Ž…—ØsC@¬£üŽïÞ¼‹u”+Éúv¯– Aúµ¯}õ/~ÿK±ø R·®"¾Ž›à ë> u¥;­B`#Ä:Êß½y—,W’õ%ì^-‚Öaq¤WWÛ)øùÆ·9]ݤ®´H·µKl‚€XG9ì»7ï2ƒåJ²¾„Ý«åB’%Œ R¾ýR,üd mätuºÒ"ÝÖ.!° bå°ïÞ¼Ë –+Éúv¯–KAzsýržìÍ5C-ÿü·ÄKätuºÒ"ÝÖ.!° bå°ïÞ¼Ë –+Éúv¯– Aúæ›ß€f@<ÈÚÁ‚,ð —¤T¬c!¸%öüë(¿ç»7ïbåJ²¾„Ý«åB¾ýö[pŒW_y\èl„y0ÔB!rººH]i‘nk—رŽrØwoÞeË•d} »WË… Å×ë€l0‡…)´óWæNİKøS÷©+-ÜkíÛ ÖQŽûîÍ»Ì`¹’¬/a÷j¹¤ÄŽ’˜”y+$EgƒuÌ%Eê> u¥…{­=B`Ä:Êqß½y—,W’õ%ì^-…”9,äBg`¿‡[“Eq‹Â-áç„€XGùÝÞ½yë(W’õ%ì^-W€”ˆ\nÕ{±ŽàÖ)αŽòû¼{ó.ÖQ®$ëKؽZ.)ËË>ù€î?ÿäC;YIñ{DNW÷©+-Òmí› ÖQûîÍ»Ì`¹’¬/a÷j¹¤D“2¤Âj,üç_}yŠ'¼,JuºÒU…رŽò°{ó.3X®$ëKؽZ.)¬Ã­9{sís“M ‰œ®îRWZ¤ÛÚ%6A@¬£öÝ›w™Ár%Y_ÂîÕr!HÍ×ñ‹ø¿1Eq ¡-±gˆ€XGùMß½yë(W’õ%ì^-‚ÔåëxðÒî&Ö±Ú{†ˆu”ßôÝ›w±Žr%Y_ÂîÕr!Ha.EXçóÛß~&ÖÑÁCE!P„€XG|‡ƒwoÞÅ:Ê•d} »WË… u¾Ž{÷Yå_þú÷ìxhËBhKì" ÖQ~ÓwoÞÅ:Ê•d} »WË… ýÕo>ƒcØ–yŠºH]i™— fB`5Ä:ʡ޽y—,W’õ%ì^-‚”Llw„ß\þ‹¿¿Ss÷Gݤ®´»=Õ/!°=bå÷`÷æ]f°\IÖ—°{µ\R‚:×±¶+@@¬£\ voÞÅ:Ê•d} »WË… %®Ã­3ûì1Jmûô£÷MºÚ{†ˆu”ßôÝ›w±Žr%Y_ÂîÕr!Hß~û-8Æ«¯¼ÖÝÄ:B[bϱŽò›¾{ó.ÖQ®$ëKؽZ.)¾r¡³æ,ë¾Ùö??þßb ¡-±gˆ€XGùMß½yë(W’õ%ì^-‚ôõ·þu$®ãâ2rººH]i‘nk—رŽrØwoÞeË•d} »WË… ÅËÁÖý°æ,NȧîRWZ¤ÛÚ%6A@¬£öÝ›w™Ár%Y_ÂîÕrYHŸ|ÀlY¶ÿsõÏÿ÷wQÎqˆÉ¯Ø=nÁ”¨ë(¿)`øÎ;ßZhÃÙ;*™wÊhýB•å(IÂʈuÌœußXdöÞÅ¥ÛîÝgõ77¥%ü©ËêJ ÷Z{„À6ˆulƒ{ÁYY¢7î\ L‡î±Žy·öÃ÷¾ÓøÊwý2”ôÁC¤G¤Õå u¥Eº­]B`Ä:6½ä¤ÌìûâåJ$èØs@@¬cÞ]æá" ´{ìþöwðØ~·²[®ËêJëöSe!Ðb-Ü…I}À*²}ÿß›t”Ÿbóîøkø'¸;ºÇ×!ÖÑDe!P‚€XG zë{uõ±±—>Q!F@¬#ŒMlÏßþ A‘¾¸¹vÛ“ˆë À#rL]ïD]i‘nk—رŽM`Ÿ}R":ŒuðqùΖ¯wƒ€XǼ[ɺöÿý<¹÷ œÃÂJI‘V—'Ô•é¶v MëØöÙ'õ”ƒ³åèÀÝ# Ö1ó–_fPú—ï¼Ï’÷.}Nð§.O¨+-ÜkíÛ Ö± î³Îú¿ Ù`lÅÏ£ƒÎ±Žy·™Œè¸8,Y‡åë ÌCsXæ©£„À±Ž!&ÍÖß°A›ÉB˜G³½UǶE@¬cþL`ax…ͳñ¬–¦Ã&­xÖ +(LÍϱŽy·Ö‹ƒ'‹R¨þ«¯¼f«±<}ú4$°.O¨+-ÔgÕ ­ëØ ùIçµØQKÐa¬ÃâH» d’@5Þ=bón1Y¾ôú»¶K÷›Ð§.O¨+-ÔgÕ ­ëØ ù©çõ9ÁÌïÁ; ®2šI`êYÔ~7ˆuÌ»•°‹Á•Y—'Ô•6ÚaU ëØüy§6Ö!Ó4½ó9J¬cµ{]÷a¬+m5t"!‰€XG&Pí4ëú:Úé•zÒb«Ý‘º<¡®´Õ@Љ„@&b™@µÓL¾ŽvîEË=ëXíîÔå u¥­‚N$2ëȪfbíÜ‹–{"Ö±ÚÝ©ËêJ[ Hd" Ö‘ T;ÍÄ:Ú¹-÷D¬cµ»S—'Ô•¶:‘ÈD@¬#¨všýüï+9X;·£Ùžˆu¬vkêò„ºÒVA'™ˆudÕN3±ŽvîEË=ëXíîÔå u¥­‚N$2ëȪfbíÜ‹–{"Ö±ÚÝ©ËêJ[ Hd" Ö‘ T;Í~úþ¿k„¥ÛÑlOÄ:V»5uyB]i«  LÄ:2j§™±¾Ûé’zÒ b«Ý”º<¡®´Õ@Љ„@&b™@µÓÌ–šëh玴Ù±ŽÞ}™šçÜÏ9ª.O¨+­‚~ ÍëØüLí€XÇTÄγ=6‹.´±ìà;ï|ë„6[?‘õd'õ9ÿ¨Š:&ÖQL‰j±ŽoJ¼KÆ:ˆ)7Ó^! <¬“H4ÔÔ§fÞQþ¤ó bópÓQ§‚€XÇ©Ü)ßOnÙ ûéWAœ!óøÃ¼£ áë(P‡7Ž€XGã7hØ=±Ž!&ªqæñ‡yGÅ{’Ü+Ö‘„H N±Ž“»}D Ê×qrwMÞyüaÞQ…W*ÖQ o±ŽÆoа{bCLT#âÌãóŽŠ÷$¹W¬# ‘œ4b'wû—¯ãäîš:¼-óøÃ¼£ ¯T¬£@Þ8bß a÷l6ßÔhü¡ÕóA`˜wT!ªb…êðÆëhü »'Ö1ÄD5B ŽÀ<þ0ï¨xO’{Å:’©ÁI# Öqr·LGa9¹»¦o‹À<þ0ï¨Â+ë(P‡7Ž€XGã7hØ=±Ž!&ªqæñ‡yGÅ{’Ü+Ö‘„H N±Ž“»}b'wËÔáÍ˜ÇæUx±b…êðÆëhü »Ça¢!A`˜wT¤9»Ä:rPR›ÓE@¬ãäîXÇÉÝ2uxsæñ‡yG^¬XG!€:¼qÄ:¿AÃî‰u 1Qˆ#0?Ì;*Þ“ä^±Ž$DjpÒˆuœÜíë8¹[¦oŽÀ<þ0ï¨Â‹ë(P‡7Ž€XGã7hؽ¿ù«?#®ƒõ>Ö&„@dÕ› %Ö1´?ª…ˆu¸þáæëÀ„jB`ð“I¬XÇ$¸ÔXä Ö‘ƒRSmðu0y6çÿ;µB ‹ÀÔY¬c*bj/’ˆu$!j­¬ƒàZë•ú#ö‡€XÇþî©®hsÄ:6¿S; Ö11µó똇›ŽÄ:"à´¹ ÖÁ]k³oê•Øb{º›º–FëhäFäwƒð9±Ž|¸ÔRÌF@¬c6t:P„ë!Ól½|ÍÞulgˆuìì†êrZ@@¬£…»0©ø:HÖ1é5B`b3@Ó!B Ž€XGŸ÷Šu4xSÔ¥]" Ö±ËÛª‹Ú±ŽmñŸqv±Ž é!0±Ž é!G@¬#ŽOk{Ÿ>} ëøéûÿÞZÇÔ!°?Ä:öwOuE›# Ö±ù-˜Ô±ŽIp©±(A@¬£=+Fë…¥ÙÊMÌ`³h¨cB`Q6yܰɋ^”„ mëØÿ©gßÄ Ní¤Ú } °Éã&Ö±åÑU„ë!Ófý&f°M(Ô+!°4›..Ï!X\ÉÅ:âøhï ˆu¬²NqnˆulÎ:H7úÁç/>þÝ‹?þó¯~éõw)ð“Ê3ÿˆuœ¹´pùb-ÜõagˆulÎ:¼F)®ÃCAA¬£‹†Ê› Ö± ì:é¾ëh‡u|óG¿|ã?®÷­oùW'Ö‘•Z.„€XÇBÀJì9# ÖÑë8g=^»XÇÕ¬Œ€XÇÊ€ët瀀X‡XG›z.ÖÑæ}9«^‰uœÕíÖÅ®ƒ€X‡XÇ:š6õ,bSSûêˆuT‡T…€X‡XG›OXG›÷å¬z%ÖqV·[»bbëhÚÔ³ˆuLELí«# ÖQR bbm>bmÞ—³ê•XÇYÝn]ì:ˆuˆu¬£iSÏ"Ö11µ¯Ž€XGuH%Pˆu4Â:>ÿÅß¿`A–ÛûþLjìÄt$óëÿþîó°ìY{÷³¤Ü9H¬ãú±b[ ®sFX+°üóßþÑ/þáØþ察͛Ô9ÿô§÷>ýè=·¨Ü“þÏÕ?ó3"üή›ë8ÿlÐá×ÿ¿{l‚܃%ퟸd±Ž`ªX±Žµ×ùαŽvXÇÿ^ý»c£dùÑb¤ýÿÿí›[cîâ2.NòìóOM÷õ›ÏâaJ´nSàçð¡a%;NݫǞÇ%‹uôÓÏõëXsq÷ˆu´À:>{òŒW°sGà8¬tOvôîMjlB¾xùˆ‹Ê±ýÏÿ7ô`(üóO>Ä ‚³âØ“ƒc„.E¸#5ìõ]}ô³Ÿós8|c‹êš§ÅÇ%Ó=±Žá=RÍʈu¬ ¸Nwˆu´À:Xa–÷µù"pG° ^'5æ(ãØO/á£OGÆ;X{Ž>8çÎ܇n8ßHx¬Çü^rï§?c9’9V¬Ã¨ÂV Ò0vxÍHó×D[çZ±ŽX‡y¾òÝ_Û2÷¸#@ *ÃÁ‘ÛøÅ‹§OŸÂ~ø_Æ›ÁРߣ”ÆÜ œÔ\"/#aÖaüÉÖÕßû:'¢²÷ùíÍ™9’‘ ÛÛƒQ?×G@¬c}ÌuÆÝ# ÖÑë°A‚H °ô[H÷&56!PçÁxððµ?ü¶ˆû¢wÒd\¢ŒÒp …¿|'÷¿Â¤d±ŽÞ½ÐÏõëXsq÷ˆu´À:P3ˆÅd#3Æ©M‡q8ØöŸ¿ý]Ì‘rWãi'oüÇ5î >8E懖´4눀£]ë Ö±Î:ËY! ÖÑëðZGH'îŽøy^cŽ2áO^¼Œñð¢îˆhµü‡˜Ò;»?i~]0jÞǺœf{"Ö‘‡¥Z-ˆ€XÇ‚àJô¹" ÖÑëxüî—™Öa¡ U0ÚQÉI‘3©= ü ÿÆÿúé;؆ÓR|ßhãY“º!ÖAR»ÖA@¬cœu–³B@¬£ÖqÔºÛ%ããïú©ñE¼œ¦zsM˜è %wKÊŽ}à'FT ÁÏØš³£‰Ù½±… [! Ö±ò:ïŽëh…ud.oº8©ñ!"”©¸–Uí–2ªí˜ßñ£ž_Ai\¦ŽCÁÑ›Á‡e,©qÓoÉþqXÛŽ‡P9hû²B¬ã%*m„€XÇFÀë´{F@¬£Ö‘³d¼WÄI9 ÂÀð‡e=u"õXÇ?ýé=†WàßÿÁ÷Ü8ËØ„[QŽL춪­þæ"F¢Ýëð·[…­ëØ ywLju´À:lYØä’ñ¦‡“sˆ¹)˜–âü Wƒ?Q¾bú:È(s –ƒˆÓG…–Vaä…Q¾™ç±áÂPÅ:òñWËMëØvt߈u´À:ìη)[ïgO{{{?{ÝOÒsÜ~‰ùøÐƈ„[êÒÛe“a©ì&÷ðWÑkÌOhÌÄꢸa—ÀG¾Ž0ª^”Yù:Öƒ[g:Ä:Z`øx#»%㫟X^ôPn®I’‚¾ñíŸà”`c‚ ‡©Úmܦw”ï ™=0΄jpP‘^3ÿ“½>eº¥R]#‡ˆuxÜTØ ±Ž­×ywŒ€XG ¬ãÌFg¬_éuRcóHMjY5øv£-?AÖqX…!›<û¥×ßæ¢è¹µRˆ7ëˆ ©]ë Ö±Î:ËY! ÖÑë`‘Y6›0K7Bä3©1rȼa¡¤öÍ?"|t×(ë %l|Ò“giCñ8 vàèé¨ë!£úÕëX jè|ëØ–uÉJ¬}}»¹©ä­=¥qO&ÃÈd£iéíå'‹¿¸Œ‡„!„[ØÆ|XKÓ =jªç¤Ûaº5b]4TÞ±ŽM`×I÷€XǶ¬ã§ïÿ»t°¡¾C)5&5îêíq=zq“[}pE· Y5è ܳ1Ö |#ϯ˜s\®Å²pðýätà ëðP¨°b[!¯ómYk°úQ(A<¥Æ¤Æ^iqS0i… &«>úÙÏ‹×ÂV} šÚÀ¹¸¤ìZ^\’g£ÛÌ—áLŠ“°ù£.;$³^¾X‡‡B…­ëØ ywLjulË:¼jñîfv‰ý gÌkŒå4<\^ç'•^Z¯EœX¥[† Ea/,…XdjåŸ1QS»!ÖÑ»/ú¹>bëc®3X‡½‘]:ñCXÑL9 éÞ¤ÆÁCs &äÈ:©½"SViì'¹ìÁÏHcæÌ¾ýö[ÖUKj<µb!Pýjˆu¬µNt>ˆu´À:Ð7ÆVx¿ãg ”‚oÊ%œÔ9¶=b‰ !+HÖâõŸ|H䧋눮t£Ã³'ùâ2ÒíIÝëˆ ©]ë Ö±Î:ËY! ÖÑë`)ÃYa=œÔ9Œ€¸åW..9¯þxFt»Ð =}ðÐ&ó†:Cc6ûÀ@ˆ µ¤~R7Ä:"Hj×:ˆu¬ƒ³ÎrVˆu4Â:zZ×ÏhL8\ž{‡÷>{LKË¿A!ÿó²ñaIÜÑó»!Ö1  *×D@¬cM´u®3A@¬£EÖqsíb<2?“gÈd †±·*½mc¡¤q1ØêþÑØ¤Úø5ŠuÄAÖÞëXdâÜëh„u"ƒhLS¿Ðâ­^9'5>uKÜ‚k·e/°[` ¦»NÉCº{sÊ=Öquõ1L¦w`¯Mo/?Å:†˜¨feÄ:V\§;Ä:¶eŸ‚6¡8ÜÌ‘ƒ{BßWpÐÅI»ÚK Þû$邨 ™ ÃQD_Øð ßęГд”î)ºå£°üc/'ÑkôrÄ:<*l…€XÇVÈë¼;F@¬#É:˜÷Áûú;?~›ïÑ·êèÆ¬R¬V\y˜ÍÊkÝ97ÈÚq2Œ²ŽI»ç%Ó9ܲ›Rެ Eñ ÐÛÌÙ—]¡áòëè\šw¤Œ^£—*Öá¡Pa+Ä:¶B^çÝ1bIÖÁÌP‘m”rP™Ã:ð'Xr0ï^°šÑ7ò¤Æ]¥eÒ sX8I3(Ägšpjˆþ6¼"ÃÁ‘®äÑruøØÔœkôÅ:<*l…€XÇVÈë¼;F@¬#‡uð"~¹œæšp•—%¬cTµ’qÝ£23yÄ>¾ÐÒ-»9¶‡¤èîSh»{sÊ=Ö1zH²Ûb£¸©rMÄ:ÖD[ç:Ä:&±Ž¿øý{ŒS¼ùæ7p!m¸mÕYθ;¢«œ“w •Ÿ¼ø ™¶Y†PËP}ë@>A#! Ô‹uDÀÑ®uëXgå¬ëH²¨ù¯H®uôüŒ;XX& 9«°·nšŸZXéÞkæ¤Æþ¨ÌdXL¶×˜3ãÑ«´Ÿ¶x=Kåú¸SâŒbTN¯R¬£ˆ~®€XÇú˜ëŒ»G@¬#É:<© ‚âFn‡]|ƒ^!'®Ã+Ò˜£j‹ÝMÁO¿kX˜Ôxxx¼ápªã¢0·M‰Geå¸Û_/ÿ^K{?ÞÄÏ—ûÊJbeøéè ˆuTQ"„À]Ä:2Y‡Ídá_~¸Þ÷ªµ­R\‡½»ã+Ýû[7©±?*³€pKŸ~äϯ¸j*A /áæš6p0Žñ“rèƒWÄ\"Ì‘¡ ,a! TßbÜucOˆu$YLJï}ç¼RÝëþð¯=ïeïY¼=‡ÿ9Á×qÈ×a“YL»b#“OWVºÁ óMœKçà÷0ÇÎÐ×M¦óbì$ö“ïÐ9¿xùÚ 9ïÝ÷St‡‡È×1ÄD5+# Ö±2à:Ý9 Ö‘dÌœµW$ß 4ðÒ$–ƒ 6‚€ÍÓŒ^!ŸuØûÚ5x“Ë W@Hý&5 ‰Ô»S?{Lg Zð –¢Ã}‡ô>0ñ£*–Ü,’ ÄF¦ÞøöOŽùI<ŒL¨ëè¡­Ÿë# Ö±>æ:ãîë˜Ä:<ýèzdÃÿÌg¨™Ô¸wŸU6sªDtoR㈜Ñ]tÃuàÀ7üz²£-©´Æ6VÂQÄ܆ZR{ºÓ ‹¡±XGIíZ±ŽupÖYÎ ±Ž$ëxôè¿‚ÌÍÓŒ^aë0+lÕWºŸ¤Ò FŽ’|™ðZ2ÒD2~Â…rŽÊìŒXG&Pj¶bËa+Ég‹€XG’uÀ%¤d³B÷› ÆzdÃÿœÄ:ŽÈäÙÃüÙápƈŠf4f6«[@vʧ^’{ÜmOrÛg´ëÈIM–E@¬cY|%ý,ëÈažHødƒˆ&³0¬à+{…i¬ã£Œ°¸ÈŠ÷ªdvcšÑC7òü*(­·ã@!,¶„ž­ÑÛçgvOî•ñC¬#$5Y±Žeñ•ô³D@¬cëÀãÙ°ÕI‰°­G6üÏI¬ƒ÷»M±\dñõåóÃ:\'/.á…ß#2mÄž˜ÓUèG1h ”ß“©—XÇTÄÔ¾:bÕ!•@! Ö‘É:˜±Â‹ØMë¸%®ðà!ó2<ÍèòYLi mnQ ü уI‰¸`ÎA#ÄŠ¸Ô‹KH…Ž*¿ ‡\Ñfš0+–¶7ÒØºÍ!ñnJˆTŠuDÀÑ®uëXgå¬ëH²øÆlð.f³`Ùð?óYÆ i(¬ÃÒbDÞõ“;7Eg°†åc¬çOŸ>Õsn܃664¿Õ÷„©+Énž.R)ÖG»ÖA 'É Oô:§³³Hó×D[çZ±Ž$ëèæëÀ³÷wïnrwx‚1Z˜À:‰/pAëøü“‘|×OiLçI-BPCDŒ¼úÊkxˆuLb8=x#óçÕÌ|”løÊ|Ö¾1™løOàñÄù-𔨠ƒdÉ™­rc1·Ó„]\Gø“ß“°Œñ=b㸨vEÄ:V[§:Ä:’¬àÞ­¼…;+¾C€~Ôš9‹gƒL¼î!ÐïŽ÷šßøõ·þ•ž#-ž’«§îȧ=›‹ëxð°··÷³ÛmBAz{?åÉŸDs8F¬cIÕ®ˆ€XÇŠ`ëTç‚€XG’u˜ïîÁÀŠËé ýè”â ðÎ^a’¯cTá0zœkt×°r´1i»^f†Ã„fÑ’Ùƒ )B\‡mPWþ0pó2nä0‹v´-=øˆH´>ºw´R¬cU®‰€XÇšhë\g‚€XG&ëè2 Æ HÈééGwW·Üë05†Jœ†ñ%GzÎ^KÉniC¬§=ìejÌ„ÍM¶ p$›À CãÔÎ{“á÷ëÜ"U¬€XÇÚˆë|g€€XÇ ÖaÔï“R¡]¦Ñ-·Â:kÔ”xéómó[IÖÓn\"–ݱ©‹KŸ˜ Ù^ËîOhÍEƒX=¸‰l,†xzâœ$QׇXGg•7A@¬cØuÒ}# Ö‘É:ˆßàyû÷Ÿ˜R¼ñ èG#¬ÃF^,óŒ¤À "aPÆ’Lí¢¼Cé= >ãCP,Ú¤×Ä~Bi쌴!pÅbP9Ñhc«눀£]ë Ö±Î:ËY! Ö‘Ã:›àéþ¯¿»A?jE“Žj†Ñ]ÃÊPc–ž§Û„sðÍèÜ)>A†f> ºQ”Pr&&9ÂÙ2Ÿ=¶9¼æ!^…¯ëðP¨°b[!¯ó$ë`$åH9˜Ãòà!C ¼µ}P‡%ê¬ør#¾´—>» 8 …¼òߥˆJsu4 gßWil£*æ2”F×a"׿0‡÷eŒëèZé>‹ª×D@¬cM´u®3A@¬#É:ŽK™Ü»O‡gP7Ÿåàú¹;Úa6®ÁЛ4¹¸Œ¨7|àÈ©,‹C"ÆMh!Lø IvÂ?Fgr>òuä ¤6‹" Ö±(¼~žˆu$Y9»`|{Êá Æ:ÀÐ×t 嬃Áœ ™š™ß8N$8\‚6l‘ðÑ^…$3=$T)ÖBFõ«! Ö±Ô:Ñù Ö‘É:FGRê°Ž±©.«Æá“~ïß\'JAèÅQào?#v\ã==„Z‚^Fw +]n®ù¶™¶n mø#ÖÆF{VB@¬c% ušsB@¬#É:\zÏÃH oXVÌ›Á¨ ïn«÷•]Gå_ÇÕÕÇ„:ô4[‡ä^e÷'§öS_IoÌ4pÁœÏ¯8ÊyéJ³òÔžÀØ c„§B{l‹'â Û,çÒz\\âÃqÞY™®×%±Ž ú¹>bëc®3$ë€TøØQà2jA £DwôȆÿ™Ã:hì8Óº a”HX„'oê#‹8jÜU]{ÅJJ·­çݽVžÔqa-–"þþá º¸ zQ^¼ÀÅA·˜÷îÔl&/V}Ø«ë!£úÕëX jè|ëH²^Ç$×2âñèÑ##îÝ}qÉtû9úË:SK Ým”uX„‰snÜNH±CFwuØe%½¸´8o„K M·qá…²qø‚(X(= M³5{­·G²$_G÷>©ÜbÝugˆuä°ÞÈ$Îb‹L¡LÍ(Ùð•9¬ÃÇmZô¦}»i&c#,ìej—Ó¸«Hó‰¿B¬cRO¼pˆóSì§Ý„¦¨ß #:!΄‰´ñ´!òuxUØ ±Ž­×ywŒ€XG’uÂAD/Êз§½BëU­1˜×Øœ \…»`¢ë¨¨ae²'°/Av  ؈&÷°ŒèÎkt؆-݈utÑPyÄ:6]'Ý7bIÖáóuðOúèÖ#þçlÖ1‰0a–ãq-%K˜9Iâ͆{“=éz6èFò,6ƒXË 2<£¯ëðP¨°b[!¯ó$ë ÿ÷Üà·ÿ­[ÁÓŒ^a*ë`JHΔRÓFߨO›k)íq× Ÿ$çñ–ìµÆ´§P³qgf.Á¥ñž‹u$‘Wƒ¥ëXaÉ?CÄ:’¬.A8‡EtX<'tÂ*ýwoØÏI¬ƒ—|æÞýx Å´˜RŽ‚ÄU÷(üƒJ{?Ú2zdÀõ„í0E%Þ“IRóÀ7Øl…—ÑX¥XGíZ±ŽupÖYÎ ±ŽÖáID‚×±±_*Lb¼‘¡`è?Ÿ¼Î*e/W}cºÄ{<¨·7×½öüäØÐ‡½t€PO‹Ù@x$«Æ¤Æˆ²[ìÛIÄ:ÂØhÏJˆu¬´NsNˆu´À:0n¼‚= à~Žæ²¸¹&4‚½$õ2=5§ÁxãC N~ k<רpr¡ßö¯½ì\!á“svË·Ær-„•ºE[Ä:ì–è»UÐpKJ¼fÅ·×D[çZ±Ž$ë`<Âf¯0 â]É™2›Õ—û:Ž‹Ñò¡)€MA¾ë©·4\Ä“pjË.N¡ˆ;˜ƒU±ÑÎ8ªlÖR˜YÇa-ûP ŽI9³eñ¢Øy±ŒöÁ*e{#àh×:ˆu¬ƒ³ÎrVˆu$YÇÛo¿Óˆl嬕;®/H-ÎÛytÑ7ND7ˆ¥?NA9®´4`(Ä¢7)ÄeXOhÆé¥‰N†Íol4ûhð½Œ^£¿±… [! Ö±ò:ïŽëH²ô&­ô~VaÌ$å-l¢ø9üð¦¶5å»ßÉ¡ |x˜[Ä:’¬RasXBßUXÈ[Ø’ŽÆ_ÇC]…„ +c5á<äv° j‘v§qT2Å ¤M´™K¬#‚¹v­ƒ€XÇ:8ë,g…€XG’u0u”<Þ$Þ muXÇÍ5C'6hB!罜TT†3\Y2:«Ë‘‚ƒ³$Íl@p‹Ÿ>C4Hdõ7'pÊ5ŠudÞ5[±Žå°•ä³E@¬#É:ÖÉMjë¿3ÂÆH?:yë.p/ýÛrïè,âF†m”Ý N*Ä Œ·s!ŠðT¸§åqA·^':?']£XG9·A@¬cÜuÖ]# Ö‘d¬3Û äèý,÷uðÊæõí#(X©Ÿîøàa€Eð–ǽ€ãWÌhC†3²a\ƒr!36|C´Äc´½Uf ·ÆD‰ÐÈ §pëð†ùÌÔkëˆÜ#íZ±ŽupÖYÎ ±Ž$ë€TøÜ¤–¡ÔjøŽ/;›Ÿ% ãÆûšoÓ½ÞÏ¡BÚûÉ­ô‡ÙB“[9Ö^÷Ý©²}KŠ>I8ÃOpø '¾T\ï¢z?‡×(Ö1ÄD5+#€–*_Çʘët»G@¬#É:|¾ËÎÑý¶”å¾2aH¶¡ ÷* ¼"8ðZØüSŸcT]-«™9‹{Ä\£-­r‚ð›kú õ²ã¬cê5ŠuDî‘v­ƒ€XÇ:8ë,g…€XG’u¬“¯ƒ×7Aý(ĉmx‰ÛÇŽ¿Çþ¸Ùµ¶ ëÖeÄux™¾0&õ…ñŸ wG|6ͤkëÅ\•k" Ö±&Ú:×™ Ö1•uàd¨ײY †*¬ìf˜Vý Ð6˰QE6œ„p‘äêöþ\½kŒS±› [! Ö±ò:ïŽëH²b'l^‰ý«ÎD þ»·Hû.ay©`7×ÌluIDŸ_½¬¬UºYW®Ö©ºryeX§[ãÊ7×#•Fb0TܱŽmp×Yw€XG’u© zÁy9ã ÌÚ L"D6|}~4)*Æ+˜  ‹èà,¡i)ó”qQáÉ.YÈ+IHº››Ežð‚L±Ž$°j°4bK#,ùgˆ€XG&ë0.ÁâïnxåÀ=X ’À+ÕÓŒ^aë8„eÆ ŸáÛMn½wA…\R8ž¸ëÞâ» `K½ôºMÈ+Î"ç/·‹K+…ëµìþë袡ò&ˆul»NºoÄ:’¬£7‡¦a+Ï÷À)Ñ#þg>ëÀ¸!·³#7/Ž‹×W íXT¸AÁ•rÕH$Í |—=ëØ·å9‰«ë8‰Û¤NžbIֱ› ‚£€÷2ÞFâËÂNÒ±E…éõwq_àrÓf™óàa(FÔÈs†Ø:,ϯâ£Hb“î²/€XǨJæ™# Ö‘d4èMZéýôÎ^!ß×V€8$-gø¦¢f.'œy1Ì»1¦a¹Â"ݶAF‘ȧÊ7+ÒX¬#Žv­ƒ€XÇ:8ë,g…€XG’uÀ%|RÅ1,ô(?'±^ܼµmsqѤåSUtQáù¢pi6ÖRˆDŽ눀£]ë Ö±Î:ËY! Ö‘d½¸ŽnnR+ù†ÕLb=­ƒ~Œlx¢·/°ú[¯•ÿî[t …w—ãÝ눣§½+ Ö±È:Ź! Ö‘d+Äu uâT“êGZâ»p97Œ<{œ\¹ž™³>ÈCZÞ„»>˜d¾3„'ûì¸>Ü\ómSh‰`ñ»†±Ž!&ªY±Ž•×éαŽ\Öq• T Ü‚.Lr¦ðüÇ„>ÿßs™:..™9˹½ |Ä:À¨z=Ä:ÖÃZg:Ä:2Y‡-=É  ¯l{…ið¥ÌVÈ:lˆå‰ÍÅeÈ1`z ¡¬ Ë\€D4,]¶W|r=zçÕyð™.ìóPÈžù¬àâ Ÿ–1Œ /6M«:\¬#„ŒêWC@¬c5¨u¢óA@¬#“uð–‡],Á:.aCòëoý+› ztñzš±á"€ù0 âÚ“ë#zJKÚGðÈʰP'üÔ NxŠÒd>,&Š‹2ø^\A¾ŽLøÔl Ä:¶@]çÜ9b™¬c9_‡i,‚—¾•mF1üІÍMr9¬{uõ1Íâa™áýž³=´ÇÍØ}ö˜L\/¹Á°ÓkŒo ™Îg‰â9Iä똎±Ž¨Œ€XGe@%Nþ«ýŸú|°i„uh‹\軋|xðpô@9‡ ¯[ÎÇœ 9ëÑ»Q‹K—Iƒ±ž{÷9 å87Èé€oÃØK„~ãâ:±Ž06Ú³b+­Óœòud²aÁÀ™€ŠŠq^ݼsã/ßyßg÷{­@=A¼»a½]‘ŸØ;êãß¹Då|ÑÁY8rÅ:lˆ 1+;XGEØ%jbópÓQB ‚€XG’u ÀaQ£Ýü`¼”qXBaL)¡X­ø±]O>Ý˹\RñÎÇ&¢v**™S“3¹uäLá8 ×øÙcÝî]ˆße±Ž ú¹>bëc®3$ëÒ‰^ S?ÌÒ«ŸÄ:Ü+øö•­ >øX‰;ø¶H‰ŠŠ ±axå8rï>‘¨£Â_&÷¸ÝÍ´\&ÛÞþùËE±°|ƒÍV¸it[%Öq‹„þn†€XÇfÐëÄûE@¬£œu a6ëàýkdƒ½­#Ã:âÁÇb¡\¹Ò4Ss¸¾¡ÀO—‘lðS±ÑycñnÛÑôÓg¡ï¶XÇrU¬€XÇÚˆë|g€€XG9ë(ñuà¯àµŽ—€W0{)S½‘‰8%BƒØ4èÄ£–ªbf‘ìãT)¸ŸcãD–²µË""ݶ˜{÷‰ÉI3"ÖQëžJÎlÄ:fC§…@±ŽrÖ›W;c„Å‚6y¹ó"ö[$7ã$ãÝmî—L,ºãñúÏžbëc®3FX³c|\÷sìCFt·ëÙc–ŠcPÃküâï+ŽkŒœóùÕHåmgÿô£÷nMHÇá Ä:BȨ~5Ä:VƒZ':Ä:ŠXǃ‡ .ŒŽ¿P™×¾16Á”U2u°¹¹«¡ ·æìa¦*3GŽƒ•æ«Bc†TV¤4°‘gÿæ¯þŒË¤àRvTu¹ˆuœjöJÅ:š½5êØé" Ö‘Ï:ð3XÊÞÎþ$¾ÔBLG‰ÇTÖA€ù:Ø\Bòë`vª¥+'ü'ñœŒkÀ|Ê5Kà¤LKénP‘POà.GÇ!ž–üdÈ[fwL¬c6t:°bµ”!àëÈa [éÕ^²öÍKî1Ê7¬r븹F¦Ï×A( ¹ú{Ô+°—ü¶j<܃½Uf»ëè®\O™k¤c½ØOÒ›Ó ¸yY­ì"KiMŠ>*j´R¬cU®‰€XÇšhë\g‚€XGë`éUc6_Ãeí8üÏw„xä³2EŒ„ùH‡î’¢~n®I<Î^ÒàôpääæšGÛNªÄgâ׬÷…#‘ âÉ¢fûí*ž>}n>aXǰÔtÄ:–ÁURϱŽ$ëð¹I ¥ GC*|¿ñíŸ÷ÀdÉg¨ ª˜«!âaxôÈ ¾°ád° Æ}ø¹cuãÂqtxÖSnÒ…n„Än‚€XÇ&°ë¤ûF@¬#É:x¿óæ…rôS|–0wôvMa9Œ’0ÂbNça¤wѤ—¤Ô°¾ûe1Ö‘œ ËØ ›}` ÓŒÈ×±oËsW'Öq·I<-Ä:’¬ƒ± ^ë¼î‡ÔÂòbáp5“|=µ!dÂ}Ë¿vwÙŠ'Ý^ú0¢nMò͵•$óu¸Ó^’Õ8¯by8©Õ‚ˆu,®DŸ+bIÖa¾[Ž¡Ë.lhB²ë0}à VÓ'Ð8ÄxD¦ÙÎVjÆnˆžesá%©èÐIó»$Ö‘•Z.„€XÇBÀJì9# Ö‘d6[vÁ„&³|çÇoóÍÑcˆéÅe•¸ŽQ%²\.Ð ˆ „^YxÉèáó*a6jã$_\ºÓ…?“‡ÅŒìëEUë" Ö±.Þ:ÛY Ö‘dø7ü¤â}:¼kL¨!˜„À ¢X‘Ÿz1¯aö+§@rRxw¥{úlÉ=\’±Ž(JFbEðé`!0†€XG’u0’¿ðLå8ÒŒƒ+ÀÊðé1äY¬ƒˆˆg àDàGŸ>¶[d+½ŽfÉ 9†5Ãéa)¹8ä;?~{ìÞΩ3"±õû^ù_ Ýæ§-PKŸÝc}Ë±Ž éºˆuÔÅSÒ„ˆu$Y9(Œc¸ïq2Jj5C¦Ñ­Éa´ÇyB8(Aqˆ¦À£ÂÌÜQý„iÐŒp ^ôl,uùhã•–¯ƒá˜¹ËøvÝ |d’{ë0²ƒªò먣„” ÖQ‚žŽ£ˆu$Y‡…RÍàõÊ[Þ^ôƒ.Ç–3Y’‡ñhŠcg:ë°XXÅèÍWi%ð+.ÐÒ’Däø•îñùN<¥XDÎp—XÇÕ¬Œ€XÇÊ€ët瀀XG’uÜ›ô¡Ç˜ÉÛHK#!¬6R8‚»À"(ºß‘,Œé°q”߈ñ }G]]õÂÉ ÂµG„ãÙ Gmè?t¥b汎ìÚµbëଳœbIÖaN Þõîÿzfjtâ:}¨6sö®ÚMK´u7ÃÆ]I¥¿` ±Ï³c,ŠkssíVέô먤ÄÌG@¬c>v:RëÈaݵì¡nûýŽ­XMÎKà¶«Üqù:: c"¥F§qõ"¼‹…`ˆAe³¸ŽZ§먅¤äÌF@¬c6t:P„ëH²0… ….»``…˜R·dI¥™³Ä…’û‚78 !§L†ÍÞòöm«¿…îïÒõx~à<~ãg­3ŠuÔBRrf# Ö1:(Bˆu$Y‡Ía ÅŽÆuøûÂ^Ù0 ¸ â@*ü^_°Îø·<¼ ßõþD™[×–bqa¨b™À©Ù) Öq wI}<1Ä:2Y¬À|F3ºß]H·s‘«ŽNAe óDº«­Õ}×OU_¼@„»ØQ˜h±Ž©ª}ˈu´|wÔ·E@¬#“uàRà Ûu2ør—itË“XA›L1¦A!ÉL¶®²Õ}×w%'Ëvj—YKr}Ä'¼$vh„¥‹†Ê› Ö± ì:é¾ëÈd6ê1úÝeÝò$Ö‘©fLMíÍN…ŸàúÈ<¼n38n™.=õÏÌ;iWì< :J" ÖQ CÄ:2Yq %Œn]¦Ñ-/Á:ŽKÍönä’3g{§êýd†oÈ-Ók9õ§XÇTÄÔ¾:bÕ!•@! Ö‘É:|\G—WÄËK°ˈΠÛ,#:c=ûÓd±ŽýÝÓ“»"±Ž“»eêpûˆud²ŽÐ–ñX‚uØœz\ºÔÃâò–4µ}M›ÚC±Ž©ˆ©}uÄ:ªC*B@¬#É:Ȉn+¾ FdÚ,—`–ÿÖár…=¿"ŽÂÍy©7_µ'B¬£{q¶=ë8Û[¯ _±Ž$ëxô葟½2,0º1d#V³ë€i°¹…ZZêÒé# Öqú÷PWÐbIÖAЦcö²sÀ=ŒÐc”xdú:&-^osXž¼øŒ^A‡Ð' £XGsÏ•:´ Ä:vqum! Ö‘dLáµÎë~H-ª°$ç/^“¡=üǦ®"L¬£­'J½Ùb;º™º”VëÈdøB¬ ‡»¨ÉôuÐŒ­«"AFtB>HfsX>ýè½Çï~Y¾Ž.z* ZˆuÔBRr„€G@¬#É: æÓèÍX±0NvõF^<Éd“¯'yˆÅrüçoÇM$΄Ÿ„šøº›š¹›kÑ…œ(b'zãÔí–ëH²^ôÆ:øÆÏÀ|øI¬’ÁO3z…LÖqTÞTÙÞÏ–uh™¾‰u,ƒ«¤N@@¬cXj*òëH²¸„ ½¸ôÜØErtpÔ$ÖqLlþäƒO> ÃyhælÞ]ÝC+±Ž=ÜÅ¿±Ž¿ê~‹ˆuä°(Ü gð Ùà›qå˜Ê:ŒÉàBñIÎ[T—û$Ö±"Ø:Õ8b㸨V Ö‘dP R}çÇoû\è½ct›äë`¦ LÆbDÝ’²—wu‡Šuìá.žø5ˆuœø T÷[D@¬#É:ȧ "²RŽI¾2ƒáëÀÄyùèÓǾ|ž±Žó¼ïM]µXGS·CÙb9¬Ãr¸ãa…‹ËrÖñÙ“gHf©—Ï?ùtè|Crö¡`³¯B¬c6t:°bµ”!àëH²R’˜Ô\Äx|óG¿äÛ¶rÖÁíøÊwíÈ̃‡¯ý៰q:γ Öqž÷½©«ëhêv¨3û@@¬#É: „vÃ\x!˜9YôÍ“Iq¨Óoo޹8X݇‚; ±ŽÙÐéÀZˆuÔBRr„€Gàÿµw-–\UÇ'+!ßË…àFÄe>ƒ0YéÊ.u#Е™M `A ¨‹$ â ‰ ®FÛ…ÁEÌÎ(Óžg æeºûÕ«[uλ÷×<šzoªnû?çÞüóºª.ë8Æ:&‘ˆo6¦¥Ö⋈pxpÇíî1Ë:âq£ñ Ñ}^Â@ÿaƒ@… YCgXǑֱ¿I6Ü#n6™þà ÎÆÅû/76޵Žÿ^\=}O\ýc#ž×±û=üë¾ò°Žüˆ ;¬ãNëˆÂ.â±]û×´ÜþJÙØ¿=Ò:Â7vÏèˆëTï½ñŠ/Rbu•îjm^‡XÇ<^ö^ëXª&G'À:î´Ži%ú½c|yc¯GZÇã/®â1áqéÈ´½»²4®&û¡è¬cô¹©@ÿYG$¡7¬ãNëˆ{Xâ1¤·¼dcÿöHë˜J*v½™¶ãæÙ°ŽËËËÞªmNXÇZö]…ëX«FÇ&À:î´Ž½EÌݘeñEÇÞ:âZ‘¸_fì¼bƒ@… YCgXG눿­Äkú érñúYc‡uÌÂeç5°Ž5¨jsp¬£ˆu<¯ÃgOâ+qÛìSwξóÎàcS÷Ó °Žô ?¬£Žu|ü‹oÆz÷ñŠÁ/%滎þf›³ëë8»” ¸>ÖQÄ:Â4ž/ïO_ù~<‚¬~ñ¬!ëX¯Æ!À:Ž¡df`¬cZs6¸ûgãqd»•îï½ÎJeg;³ŽÎzŽÝaç˜51'À:*XGLn»t\]ÅSOãFÝØˆ·ñañâY5<Ö±*^C€uCÉ>Ì"À:JXdzfÄR,ñ]GXG¬to¿ ”uÌÈv^ƒëXƒª6'À:*XGaÜ*¦¯x*H¸G|é¡2' ûéXGz ÐÖQÄ:⛟ÿùI,pÿ·~ÏHüÇwƒ@… YCgXGëØÕÕÓGñG–x]Å*´Ãÿ°ŽáK ëÈϺ#À:ŠXÇ.~KÍînž}ù~,nû|±ûõƒut7Ùœ_‡XÇùåLÄå °Ž"Öñ½¯ÞûúO~?­9âñàÁëQ;á!Ÿüõ·å‹h•YÇ*X5:‡ë˜C˾E€u±Ž¸Žôââñ”³Ÿ½ñΫ÷_‰íøpº‘ö¨\öµëè+ŸgÙÖq–itm¬£ˆu|ã[ߎ¥fwBö$¾÷Ø­?ûôÑtKKí Z+:Ö±YíM€uÊŽK€u±ŽÝE÷^Š+:¦ø½ÛøÚc%¸1Xǘy/ÕkÖQ*‚éƒë(bq-Ç÷÷¸yöú+.óö‡u ›ú:gur!’n°Ž"Öñüÿÿ K7Õµ¤#¬c =Ç6!À:š`Ô× °Ž"Ö—s<úõ÷c™ûø#‹•î£DYÇõqj;…ëHÁî¤}`E¬ÃJ÷uñv{¬c{æÎØ=ÖQÁ:®¯tÿö?²Ò}Œ;ÖÑýäS¿ƒ¬£~ŽDxvXGëˆÉ-n`‰â™Öœxž]95 ˜u4„©©Ó°ŽÓ¸9 [°ŽÖa¥û/Õ(ëølM€ulMÜù À:*XGÚ~¥û~`¥ûÂ:˜~ªw‘uTÏøÎë(bû•îãÚxpG\Úq†ÕÔ2dÖÑ’¦¶N"À:NÂæ n#À:ŠXÇóEf§\={²[ï~ìÖ1vþKôžu”Hƒ ú"À:ŠXG<ÿüÉ¿ÅÍ,ñúìš..í«Öæõ†uÌãeï°Ž jrt¬£ˆuL‹°Ä#¦ë`£ÏMúÏ: $A½`E¬#V·Ó˜–b‰ß¬ƒuô6לaXÇ&MÈÕ °Ž"Ö«ÛÇm,S¹Ä\Ç:XGõ¹c€øXÇIÖÅ­ °Ž Ö1iÆÅÅã)ýŸ~þô+ßy{ëR(v>ÖQ,!#†Ã:F̺>¯L€uT°Ž¸m6Vº¿þßÙødðŸë4G¡ûYXGyçí˜ë¨`Q`¿¸ú÷ð¦q} ±Žë4l§`)Ø´o¬£ˆuô]f'ôŽuœÍ!m °Ž¶<µ†@`¬£æ@`5ó2TT¬c¨tëì6XëØ¦Òæž…uÌ%fÿæXGs¤D€u°Žš£€uÔÌËPQ±Ž¡Ò­³Û`¬c›J›{Ö1—˜ý›`Í‘jÖÁ:jŽÖQ3/CEÅ:†J·ÎnC€u°Žm*mîYXÇ\böoN€u4GªAXë¨9 XGͼ ë*Ý:» ÖÁ:¶©´¹gas‰Ù¿9ÖÑ©`¬£æ(`5ó2TT¬c¨tëì6XëØ¦Òæž…uÌ%fÿæXGs¤D€u°Žš£€uÔÌËPQ±Ž¡Ò­³Û`¬c›J›{Ö1—˜ý›`Í‘jÖÁ:jŽÖQ3/CEÅ:†J·ÎnC€u°Žm*mîYXÇ\böoN€u4GªAXë¨9 XGͼ ë*Ý:» ÖÁ:¶©´¹gas‰Ù¿9ÖÑ©`¬£æ(`5ó2TT¬c¨tëì6XëØ¦Òæž…uÌ%fÿæXGs¤D€u°Žš£€uÔÌËPQ±Ž¡Ò­³Û`¬c›J›{Ö1—˜ý›`Í‘jÖÁ:jŽÖQ3/CEÅ:†J·ÎnC€u^óÍ7š¿^{í§¡4^'Øf8 7`7‘ñ9'`'£kx`LnñjØ ¦@`9Ö±œ¡8 À:€¤¼ýÍÞ|ïýwSNí¤ pÖqŸ#p2Öq2º†þèßWÃ5…Ë °Žå µ€ÀÖqdû·_½ÿJ¼bcû³;#ÜD€uÜDÆçœL€uœŒ®ÕqëdqÕe«6µƒË °Žå µ€ÀÖqdã·Ÿ_}[™¬#~?}¶ñùn$À:nDã8•ë8•\›ã>úøaÈÆt]GlÄÛ6íj`‹jC¬ãȶïãQ!¡ñ;îñ{Ûó;ÜH€u܈Æ? p*Öq*¹ÇMsZÜ3²¯¿|øV|Ý6hZ °˜ëXŒP`‡D6|¾±»–ã³O¦ï:.//ãmddÜ n$À:nDã8•ë8•\ƒã¦¿­DCÓw±1ýÁ¥AÓš@ÅXÇb„@àë8$²Õûé1Óå£{ë˜..õàŽ­’à<ÜF€uÜFÇ¿!pÖq¶ÅÓ9⻎ø«J´µ·ŽØŽã  N  XF€u,ãçh^@€u¼Ê&Å×>ü Nup9G|èþÙM2à$ÜA€uÜÈ?#0Ÿë˜Ï¬ñ)3[ã>h ¤ŒÍ˜“{d©O<'À:ÒK!efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk efKﵨO el²Žú…!Â%XÇzMŽM™ÙšD®ú&26YGßE¥w¬#½Rf¶ô^ úRÆ&ë¨_"\B€u,¡×䨔™­IäA o)c“uô]TzÇ:Òk`šÙÞ{ÿÝ‹‹Ç^ P‡ÀG?|õþ+ñ{ËY‚ulIÛ¹¶'À:¶g~pÆËˢټ@ &° ƒ1»ê[Ö±*^§`é)ˆâëŽ:ÿ'¸N`ã)‚ul Üé6&À:6ît €À-XÇ-püSXGIÔè†ëè&•:òB¬ã…X|ˆ¤`)Øt3¬c3ÔN„ÜI€u܉ÈgM€uœuú`%Tw°Ž Þ"€‰XG"|§Þ€ëزS €G`G‚²Û™`gš8a#€@—XG—iÕ©=Ö±GaH'À:ÒS €U °ŽUñj˜E€uÌÂeç³#À:Î.eFŽ °ŽŽ“«kA€u(@ ÖQ'"YƒëXƒª6@Ó°ŽÓ¸9ê\°ŽsÉ”8@`¬c„,ÜGÖ1röõª`Õ2"ž¶XG[žZC–`Kè9¶>ÖQ?G"Dq°Žqr=fOYǘy×k¨I€uÔÌ‹¨Z`­HjXN€u,g¨…ÊXGåìˆ F#À:FËøhýe£e\@ 2ÖQ9;b[N€u,g¨@ ÖÑŠ¤vj`5ó"*“ë3ïãôšuŒ“k=EúXGý‰p Ö±„žc@¶XG[žZ«F€uTˈx@`d¬cäìÐwÖ1B–õÎ…ë8—L‰ó4¬ã4nŽBÖ À:Ö ªÍ:XG\ˆ`j o¬£ïüêœÖq^ùí\¬c.1û#€ë`ë±ÕrQáo½õK/@"*ü§A  € € € € € € € € € € € € € € € € € € € € € € € € € € € € € °ÿ\Ì(… endstream endobj 90 0 obj 109654 endobj 92 0 obj << /Length 93 0 R /Filter /FlateDecode >> stream x½WMs›0½ó+öÐ5Ó`Áv&94mfzLCÛC’ñP¬$´$§Ídòß+!Œ 20ÅlïÛ}»Ú·ûð®|aÁˆ@Áàdàœq 1/¿rÇêWì–—~Xßò.±â>†€‰2æ–ï6ö B˜‚sî äSoáý)Ä‘€O‘ˆ.óE3ç,O²ðiÎóôÂ_ÖçP»UC”6ë; €˜¬- 8âßуï,›&ÙÝx¼Â—àÇ 6‚îŠÕÆxØvÁpQ$±è€Ðµ¢"“ñ> ¼`-DÅ]ÉRúïæ‘¸‡@ÏSæd·ÜQOœU®œXÖÉDÈBáÁÑñµiƺŠÅù«h«j‰gçÿ¡V\“hWÂáä®Ð·¯¯ëô<™1tåóK™ìy†n{ ã‘Á‚rå>â“8ϸˆ2Q:µùÑ„[F;°15³¯°ÔÅYñÈ …ô ÑlÁÔG]/½Äé5ŽO•m 0e³$M„®¯ù¿‡öà7ª­j†Kü8Ÿ-Òl’SÃ…+”E©Ì=*9™Ä,Ý(—ö¯|Ô¨Â%2Ëí¥&F°™KÜ‹ajÒ¡s©T•Lî©UÄ7™1„r Y¶Ÿ.Û$h0V•ÚNêØÙÌÉ0Z­aC!ì ÃZ7ÎØ´Cã{ÔGJDU¢%J˼À³:#{&†6%¿îÉtòói§®o6õGFftçS­Mw|-|(dEOý–®™4zé+tØ’ºiIÁÜ’±ÎQ’6'“´.j xEÛ—L°;V”Ìí/ ^s8©Kbæ:©'‡ö!•ò+· /×Zµ~w0<"çcµ€Ð€øÞHî»m${>ØÊƒö˜þæj°ÒY½Ül¾&|J(ÕàíðÑJ·Æp¹gÙž¬Ú†·QºnÃrjin*ÝÅ?§~2ç endstream endobj 93 0 obj 743 endobj 91 0 obj << /Type /Page /Parent 85 0 R /Resources 94 0 R /Contents 92 0 R /MediaBox [0 0 612 792] >> endobj 94 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 96 0 obj << /Length 97 0 R /Filter /FlateDecode >> stream x+TT(T0P05Ó3µ´´´P076W0±´Ð3W(JUWÈSÐH-JN-()MÌQ(Ê+Ö)0SЩ5ë055Õ3æJÎUÐ÷Ì5SpÉšú|û endstream endobj 97 0 obj 88 endobj 95 0 obj << /Type /Page /Parent 85 0 R /Resources 98 0 R /Contents 96 0 R /MediaBox [0 0 792 612] >> endobj 98 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im6 99 0 R >> >> endobj 99 0 obj << /Length 100 0 R /Type /XObject /Subtype /Image /Width 941 /Height 1391 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xì½M¯%Mrß×­…1è/ãà­6 Žc-¼âÎ2¸àÂÐôJ„aQ B/ˆIx'Ê |‘h..Æ” MŽhp—x@“œñÓ”®GYÿ{£³«²¢2ëTÞs²N4 §³²¢2#"_⟑Qy¿ü2þ…B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4 „B¡Ð@h 4p‘~ë·~ë7ýWøíñï—¾ñ‹=Š2C¡Ð@h 4 „îGÝõ—Qà}û:]ÿú_ÿÊþñ×+'¡Ð@h 4 „B¡O€UïñϺÂ`¼Áƒ/hœx54 „B¡ÐÀ½k`\|ï-ò‡B¡Ð@h 4 \ qapxƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höx54 „B¡ÐÀ½k`P±Á÷ÞqCþÐ@h 4 „B—i`P±Á—5{¼ „B¡Ð@hàÞ50. ¾÷– ùC¡Ð@h 4 „.ÐÀ¸08bƒ/höxõ øÞ÷þâ‹/â „B¡c5Àìz…9=ª<……Á|ŠÞwGB|ÿ?üú×2®Ð@h 4è¡põY”õ8 ƒ#6ø¸.%½„pV0óë[¿{¬$J „Bw®?úö0»¢„—˜Ê£ŽÓi`\|º¦άÁà˜¨ÏÜÆ![h 4p Äìz ­Ÿ§ÎqapÄŸ§Þ$1QßA#‡ˆ¡ÐÀ4³ë”~¢*…Á|¢>x¢ÄD}ÍB†B/®˜]_\姪pP±Á§ê…w LLÔwÐÈ!bh 4p Äìz¥Ÿ¨Êqað‰!D9¿b¢>‡„¡ÐÀ54³ë5´~ž:Ç…Á|ž^x’ÄD}"†BWÐ@Ì®WPú‰ªGlð‰úà]ˆõ]4s ¼¸bv}q•ŸªÂAapÄŸªÞ01QßA#‡ˆ¡ÐÀ4³ë”~¢*Ç…Á'j„åüˆ‰úüm†B×Ð@Ì®×ÐúyêGlðyzáHõ4rˆ \A1»^Aé'ªrP±Á'êƒw!JLÔwÑÌ!dh 4ðâˆÙõÅU~ª …Á|ª^xÂÄD}"†BWÐ@Ì®WPú‰ªŸ¨B”ók &êó·qH \C1»^Cëç©s\±Áçé…w ILÔwÐÈ!bh 4p Äìz¥Ÿ¨ÊAapÄŸ¨Þ…(1QßE3‡¡ÐÀ‹k f×Wù©*Glð©záõ4rˆ \A1»^Aé'ªr\|¢FQί˜¨Ï߯!ah 4p Äìz ­Ÿ§ÎqapÄŸ§Þ$1QßA#‡ˆ¡ÐÀ4³ë”~¢*…Á|¢>x¢ÄD}ÍB†B/®˜]_\姪pP±Á§ê…w LLÔwÐÈ!bh 4p Äìz¥Ÿ¨Êqað‰!D9¿b¢>‡„¡ÐÀ54³ë5´~ž:Ç…Á|ž^x’ÄD}"†BWÐ@Ì®WPú‰ªGlð‰úà]ˆõ]4s ¼¸bv}q•ŸªÂëÂàï~ë_ýÑ·ÿ`DZÁ§ê…w LLÔwÐÈ!bh 4p Äìz¥Ÿ¨Ê+Â`<ºÿÃõêßÿ³¿ûþÍ?mEÂÀà5Bˆr~ ÄD}þ6 C¡kh f×khý ŽsƒOÒïCŒ˜¨ï£CÊÐ@hà¥5³ëKkü\õ]ë[¿KäÃýþ¿ýÃͯä"6ø\ÝðüÒÄD}þ6 C¡kh f×khýW7<¿41QŸ¿CÂÐ@hàˆÙõZ?OW„Áú ¶Ïâ,ñÍ_üi#X&Â|ž.x’ÄD}íR†B/­˜]_Zãçªï`ðû÷ÿÇ„~ŸN‡ OVc‰~-'bƒÏÕ Ï/MLÔçoã04¸†bv½†ÖÏSç-À`^¾˜s,`LŽáÞY"¼Áçé‚÷!ILÔ÷ÑÎ!eh 4ðÒˆÙõ¥5~®ún! â%HlìÃàsµCHsr ÄD}òñB¡+i f×+)þ$ÕÞ þéŸý@¯NNûú×Ooò¿y7óç·á >Iÿ»1b¢¾›¦AC¡Õ@Ì®/ªîÓUv 0˜sÒ,0‡°`ð/ÿê7sÜ;KGlðézâÉЉúä â…BWÒ@Ì®WRüIª½ Ä%˜“ÓtŠéwf¸wvÞà“ô¿»#&ê»iê44xQ Äìú¢ê>]e·ƒ9CƒíÒíæi§kŠè̈‰úÌ­²…B×Ó€fW|e×c!jX·ƒu.„΋Ègàü6¼Á÷¹»dýåa0C›Ø¡Nòã_h` 1åüæ¯ÿJ¿ëÜ–YޝЏ†è3&‡èœçf’é$ %çÀÕIÿ/ÿóÿ¤oâ818¿œW"6¸S«E±4p쌠 Å:´S?‰b×@?w,«`à¿ùþÏvº°Çr{k¥1Áþí_ÿÖ¸Úäg”ι)ÈÐýZ¡³ˆ\ošÃ Ýëîù€ÁwØè!ò-h Ÿ;VºÞ0ø?ÿøÇÇ2¥]®Q:çå’Þr ýZ¡+‚sƒƒÿü;ÛxøôkÛ[î3ÁÛ  Þ¡´x%4p¹úÙ¸ËyËKè ƒóº"}#¥sÞˆº:±Ñ¯*a0èW§¥qn05#¿çpxƒ;õ‡(¶“wRlð5ÐÏÆùõ¶>í ƒÃÜÚ"/@?Jç|U\±Š~­P ƒÁº?óoæ_ÆYÚÁ|Å>UïÐ@ÀàJ‹WB—k Ÿ»œ·¼„Þ08¯+Ò7¢Q:稫ýZ¡wíÐà<áÀàðwêQl'  î¤Ø(64àk Ÿóëm}Ú‡7¸µE^€~”Îùª¸býZ¡ ƒxÿô·þ?ü›ª‹Ûo}ëw}|E¥EÕ¡V Ü3 Ö nÕXЇÑ@?w{VHolù‰­¾~Ž}:JçsVÂìvYN?·¬ë’œÞ0øÞVßýøaõÑâÁ¬¥f· òód0?üð¯¾S”g”ÎYdþ4™ýZ¡ógk~ónìÀàðŸ¦Þ‰ w ƒÚ¿÷/^ýÇùßq±ÝÃme‹c%sxùÙ[%ˆµJlo>>ÀÃÿöß¼â"á[á&¶›ˆÚD Û_ö#†Ý–Âù[HÿéÏþÝ—À$®Ç‡ÿÕÿEâŸü½WÌê¾à^»OoþêO}*™*¸õ ìgãüz[Ÿö†Á­Þà͆@@ZùOûçý¡dzhk¸ÇFtê6S¢ft·Ñ 8¡|ú›/ìgO§nÏ[Ìü…Üb=£tÎ"ó§Éì× õ0X'Ep€°ƒ{g"6ø4=ðN¹güwþáoü׿øá¿ÿßÿ„„ƒ1”fGÐX‘˜³1¾³nóýü°Hœ“gÅZN¸Hp›?¥)­ží&âYE›·Ml÷#†Ï¦Â_½}O3}õ+_ãR/Š*âšv7EQªÌôG—¶Výlœ±tH¢7 ®a²©!(å«9ÃàU¿Š¦†ƒ—V¦ùÔÊ>Œo¥÷YÍŸ2#±O’N#^ó§³4l  .ê¥ Þ-v{Þ¥sÎÄ<Ùm¿V äv]»MìÕëï~ë_–ÎÒÅDxƒOÖO/Î}Âà¿þÑß2´ÎÉDNŽAV»Ë¶ÆÓ"7 ÄùÇO.DÜ.‰™R¾ÜŒÏ¿kÄö:¶Œ·¬vþ¸3·¹L‰J¶wÏ*Ú¼mb»1|6ý7þñ?`ñG?ú’‹·Àâßr<ÐÔî¦+Ü& êßùþ%ó 8±GÅD?W¬nwfoìÀÈ} ¤´,Céþí ²ú›,õ ÷·ý}†'í †¤ùR+—¦Su+½½X“÷"bÂO’t}Þ 4ž¦ëí{Vý¼õÓ?û dκ}^é(3çù|é~­@ÉE»ÌÔr‰¥[fùµ¤´œˆ >_W<·D÷ ƒ±¼9îÌ1¸lhŒdÉý;ýEuQòKæ’˜I2£±D‘Ø^²Åi¬œÙ­‘)QÉöâYE›·3>g·³×gOg·—óÙí¬pniŽ/¾xr’Pë_9Ȉ›Ú]o [;’ùÝü‰XLô³qÅêvgö†Ác;â©4Ö¡Ó?Z&æÊ›&¯±©áÔ»x䌭§J&3/0O·ÒçïúéÙêo³ÛÃ0Kƒ¿üÿR©†Þéw€ÐÅ£tÎ"ó§Éì× õ0˜sƒ5‚f¿z—‰ðŸ¦Þ‰ ÷ ƒeDdä»ÃF,[\Ÿ$ì2#2£ÇÄ%äÝQæ·B¶8v`‰|v-™mÈÌi,]ɶ蛈­ŠÊDÛýˆá¶©pèQ¯m³ÓÇ­ ÉuNóÑÊäX;*GÄE ‡P2L|€l"°È*RZf?gU’è ƒsÍÏÞÑ*å+€&“gxV²Ý65œu6¾bV¦|J°Òf‰VúÙëέ¢­èi¢Ñ®“£Iø´ÕßæeŽÒ9àQ¿V¨‡Á \öfVÃ9F8bƒOÐ÷îJ„„ÁlO'˜«—ËÌÍœÊu¨l_Æñˆ.ª«l÷ä.yƒ‹¯×S`ƒH0á‹"³‰í&âµüz¶)¤qkáÚ>f—œ¸JÀ¾GÆüÑfS²ªJ»o߳׬S†ò×—é~6nY×%9½ap+o› A©¿Mñö,g6˯o¸k ©}û>-o§ˆ\¿ÿ´Òo²jØé;‚)òy³'£­þX¦mSË(ÓrÊD¿V¨‡ÁÀ]Üô4Ì¥.n•I*ZäJ+|Ê& ¡FÔ6…ý2s¼€›ÝF&mæjÝÚ@+Ž>2ë pHßÇñ«¤*eÇ_WOü¿~økŸ“ ÍÿeÚž$Aù}ÛMÄN¥ÅGMl÷#†·¦ÂQoB˜¡}mç‚×´;4l ðKk÷òûÙ¸¼–ËÓ½ap}H–š†£Ö`SN}é}yQ;›Ì·ÒK› --›Á$FEÎ?mmëû8:¿OL9£tNGä<ê× ›VØ,,^)l±:›ý‚„uðØ(-±Á'è{w%ÂÍÂ`q3<ììÅ4Á`ke‚¸6À*¡†[ß›[–PÉvë%¦#ŒØc­¯¥Šíç*›ˆŸ_ªø¿‰í~ÄpZ]8ª€ÜÀIú¤ñèÒö&@êg㎨7 ®ä­2@*ÇUÿÔõU@¿Ùpõí©ÞêÎYÉ'd0É”ÅpЪLѳî{ze«ÛÒ9ë56"e¿V¨„ÁX[>‹K¶øùÜ`m‚‰Ç‰|¶½ ýZ¢Þ5b£ÏçÓÀ Â`†Û”ÉC;écx˜¡‡˜?nËDýdsPß¡ëÀ^Šu—8@y]{ÔT2oi›Rb‚ù¹µ¢–‰¦Â›ˆ—uù9­l›K¡FÆzb˜l⇘ET’ "GLBeŸ oüàÏþP§L“Öè[µÝÏÆ­q¸/¿7 Þ†ðýñãÔÆÈfSS[´÷ëœÍ 4˜­žY¿ Ûìö°1JçlÖØP/ôk…JŒ…Õ #XÖ–C”““.Zá¡ÔÌÞ»nÛ°3Öð`(hA£_çÜÂzŒ-c* „íBª Ø"Â!*Cöz€(ŒNrI­ÿ«,Ù Þ³Î. níÑ2ÑTxñ².?§‰í~Ä0ÙT8 Íb pËE‚[GLH‹@ÃΠfkÕv?çH´ãQo\ÃëPl.7/ ª‹£ÕŠjj‹&â¦Î?­ô&Âfâiõ7…@ã£cjr^IOõ§?~`Yçw{Ê¥s:"ŸàQ¿V d³³~BŸ90‡LrèÛ¤-ßõVøm"œ@· ƒ5¬˜´ @b¸ÙeÃm™¨€øC(N~ùæZ·Å#•wÊÁM@qÒfYᵦ¯/Y%½nÅ™Åòx“m^o".VçdZᢩdûpb lâzIÒˆ„S ÙŠ«å¯J&Ÿ9Ÿ½vŸQû 66jšFuõ³qKY.Éé ƒ×Tj<ƒxÑ*}L96l`–hj‹}Ä9'”0cÀn­ðJz{±&ÁÒ€¬ ¨Èá„§ aòé¼Çéã>¿ŠQ:§/ÅèOûµB= ƤÒy@¼8 0Ä‚¾¬ÓÊ£üö}Že‘#6xôŽwoüß& &gá–Æš®)<‰ï›–è×r*a°ŽÂBÉYªc8;MO±¡ø-Ò·–¬Ò>…?>ÀRS]SáMÄEYœÌ&¶ûÃaSáÐ3™½¤¢51­dAj}š†Ì"ým÷³qEwgö†Ácà:E+ܤ¢$"‚†pÀsS[4[—xâ٭дÒ;ª˜=š- t‹,32»ÕŠ/Mž?úRë{TLŒÒ9‹ÌŸ&³_+ÔÃ`yƒqÅË‚%Ì“¨´Â§i¦dt Ü Æì~6âžÑ¯ÿqœ†aåĆ‚{…±q’ :ÂiJ¾Š5ì[áÖ’©Ï*;!á’ßûÍ»5ßTx±#ûÚ£z¶›dl%n¢—wÎÎE¡ó#Åš€I“„NEùÎ?ÿGÜÒ4ª±Ÿ[“h_~o¼¦R¸eÈ£sŽ4dxÒ@b®ÍÑÚÔó›ˆa©©Ûï ¯l#õd[>£gÞ L&ö³T¸ßíE3Jç¬T× dýZ¡³}FÏ)^k.)&ÌAlß§n8 žÐ/c°ýÚR´ÓÖxEøÔZg¦1Æ©Ñéì9Úž,>j3(ÅWšJ¦b-ãÎE‚Ûb±Êl*¼‰Ø©´ø¨‰í~ÄðV_¸¼sf\€@¨híO™ç ƒ¡é“3íí{ÿ8‘Vmk ÜH~o숙þÌßô¥šàérG+6µEq}g“\­ôŽ6òG ½¿yÇÖk6Θ¾ø¬8'ÈÓòÛêÏïözq”Ι‹y¾t¿V d3š‡'ê­ðùš,$Q7ƒù,ôË/ÓP¦z⑸pÛn#Ü:1\ØÐÎfó‰Xß锉‡ªU– ÐbqQ8µ” Ïrw°]ÉIVÉvníMêšJ¦êzz`‰}פ@{\öàaiKH>µÝã}Øýl\±ºÝ™½ap®Ø%“:ÚKÐ|5£ÕŠâ]Ÿ¾­áZ;g+½ñ½•ÀÖ2ÁÁÀ3[ýmœžÒ9kd—¦_+t…Á8²üA=n‹ç§ÔÀ Â`[œ{ŸÜŸ_ìîÁ2QƒA,€¾á"ñ À”šYÄàp®5bίÀ›ÊáLÎç‹3%È,ù)2ÓÑÓ+$¸ýôl‘ÚÁv¥Œ‹ª¶2ÀÄW³3K‚Ëy¡IF4f A-~ÓÌ*…Øç$§o"¶Mj·– £²d£{p‘àÖýl\±ºÝ™½aðnÆÊ/.–¢›­ÜÔp­³•¾,ÔJ.Ké+¹èöö`¹j«¿M…PÛ(sE1'Éî× ”¼´žGåÔ[á“´Sˆ1¸n ¡Rõq¦J¡ $œÑZ?…KPn–1U§=Ùb¼<âƒ¾ÅÆöñ«o9%ó(;ýÕ¼š¸w°])£˜ÄDæ—Ã9Èy‘4­V¦oÏq:­Ñ7ÉHiÒžè7 5‰}#OöÞD\ç),sñŒzé«8Q¸:í‚䳌~6î³j.¾é ƒð–óN·Ô­úgþÈÒïL‡´[%Ö–-FÖÔp­³•>}&²±š'xª`iæC¤F gæ/nŽˆG霹\çK÷k…Þ0ø|mX7 ƒ™Û™Ø¹ÀÃ`K.rôÕêò„Æ•0 BÉÄ]¨eÓöâʹÁˆ˜mw>”£jEb¬¹öm!á{áÚŒ:¦|>ÿŒb¡O…¯´µƒíJ“°ª D.9uB2•¦§Ÿÿ„aÖÀ¤´pÑ~½ŒÔ#Õé—Ý^J6”›s#§±t’þžÂäN׌9™ÒMÄË×óm1ç9¤…µØЧŽ0cšŸQÚm?gU’è ƒk˜Ô—q4.=Ή³½ø–& k¿K¨„Ö†«ìœÆ^}ˤü´ã0-?™”4L¬Òe‚ D›ë^¥s.%=SN¿Vè ƒ+×¶gj¬e\ Ü, ÖGâ@ , `0³½ü×Ã`!ÌN Îng­iVƒƒ³¨š§pµF¬§ö5Š/f€f%s+——öݬF)7±ÝD žL¥7ïäÏ”K3¹¾Y†ü‹ÂŸ*Pá0_s˜X“Œè„çéê‡õx(NªTÍ£¤áé+*s “¹ÔsñòõµÑäŒõØ$#8'>›®Br>IËý>Ë\»m"¶B´w`·JãjcE  ?£™Ýö³q³Š.¼í ƒ—½«È°‘Y¢H¶Ìô[¹µáZ;g%½/Tñ)ジæCø Ч82*AÈȺŒEÊ1½Ô•åŒÒ9áS&úµBo|Êæ¡Îª[†Á`Nü J] ®ämûö8W= ÆGÄ×ÓµÒ²$ІÆ—48R ‡½eOg ŠJHxB‰8^ü’õ®‚ŠùåÂnÎ Ìo›Øn"Îk©I#ÜòO>·M1ëeTÉürQ¸¯«X|t>Ë\»m"¶BÖP.W¿51áýlœ±zH¢7 >„I§zƒ×!hj¸ÖÎÙJïð™?¢Xf†sý 4sÒª}–YÓíGéœ3ÑNvÛ¯zÃ`:ÞÉÚ"Ä9±nsDî]–&q) ^©”Ú—¬Öÿ±±H¸ÂÓÐþøÛuÚô$ÅZèÌ´b]Ÿ¿œÎËj_@ŠEe/~Vjñgo~ºIR¬ÿ›±½N˜žÌˆWeT)3¡f·Ëšf³Û%ýã!Çè|UÀÇâ1fïAŒ›—L˜_ƒUy»¯ÑX±ýlœUqH¢7 Þè »d ¥V·T`}ÃÍ‹Ýêl­ô9wówóg_¦%sÕ©’Q:çç*9Û]¿Vè ƒ#6ølñìòÜ2 Öw1ÚÙÏ/Á¼Ë"Ö¶sM‚HTÿƒ&jçë<Â!øºD\9ýˆêJÐý°×¼áX½&¶›ˆ?c-³élýûh¿†m+¼‰½Ñ"´ Í´Ù4TÑFÿñƒµ‰bë @Z\a-ö‹dšDËDk»÷³qKÞ.Éé ƒ/ámí]ZŠÚNa*ÅV¶w›ŽAaã”1Bß³rЉ6ú–È:ΗkÎÏt…Ô’ÓpÿÒ9]!†دºÂàú=Ùá[(8…nóùæŒQ¶˜ó ”²vm@öˆ›Jž OÑ „¡®#¬OS<ÆtZúkýL3:†OìT’?¯õa< ǘÉx#µôCòzÛMÄ9o°agVÐ1ÖtRÏ6…7‹êM×/í7ÍútØÈÛ÷4¢¾b+¶Ž6šÙkN}c: 85è ž«k–®ow½ØÏÆÍ»ð¶7 îáÒ$0†æÓE*¶²©¥©áèìOiqD‚[+§˜h¢¯€ªˆé‘¦Ð2c°fô!1O)U¤^ýæ"¹µÌQ:§1|ÊD¿Vè ƒOÙ!ÔY5pË0˜Ó!0"„¯Þeþ& Ææj3‘’A¶Üro€RBé ÎÉ̯ŽM+v }?E)ÈGJf‘X±»ˆÈ' À-¬Õ4±ÝD c|Nüæ-‰)W¹Î¸¸m^¯—ÑêB4š‰_§iôJ=½Z”«ÓzaåPbŸXË(M‚Š”^þ6µ»^ïgã–ì]’Ó_ÂÛڻ氅@­Ì(.·6\úèlZ=aì-\Cß:Á´ÌBtN:$WZ*NȦ( 2ÙÉbna4ñ!éw{hF霒ýZ¡7 ^š°³¶QÈu Ü2 f®æÂFÜåÏXº˜Ø„Áj2Ù¾ :ýÃh}°Ö¾O¶fúê-uÛ'ÜK€[`°<Ék&XåóNþäV{¾‰ízâd©ß¼ƒóäó”o|ËC«õl7‹m –Þlš&zA}kw4åš¶a›§v´¿¢ÙÑîýlœø¯7 îa1ñvÒ1¤& Úq­–Ö†ÃÌ0Qiz×Wu }ë=c–å3›€[êk²ÕŸ†ŒÓí‘h”Îé+ô§ýZ¡+ ŽØàÑ;Þ½ñÓ0xÚ’‚â÷ȯ"Vf% NpèÍ;âj67e5‡0(ó.{‹J¦X\ÙÀ`<«T´$Ësð F c7±]O ¥ÜDJðË%Úó9K׳͋õÄTÆh¡äVzÔk)mj»­ÃSB‚Ùw¦Ý9y³išÚ½Ÿ›µÔ…·½að…ì_§¥h\u!M E22Õ*.Ÿ x7Á`·ÿTÒÃCÓ„ž+_‡ú‡™kqm«¿Ô“]¶m”ι֬çÈï× ]ap¥>G…'ÐÀ-Ã`í'bÑf×å0˜†S ħÂu¯ %Æج ë4½‚‡á™ÓÞÃHáózsÔlJ\Ïv“ŒKé¾­lb»•ø³ØÎõ¦Û^OŸÜÝoßsÚ*ï" ÄKÙ-'mOË%Ú%TÃ-°q}»÷³qKÞ.Éé ƒ_—°W|7ï¸Li”"™2ëNxÒÎοVú¼¨Í˜/-™7Ø~zz=ûÎÎ DôaíymödÞ2­„H¼¼úµ%;fôÂGÀà—×UÔØ­[†Á Fü®³ë¨?Ÿ‘k sæo…±ƒ@XÈLJ” = V~ž¼ºb#ô¾aÍßÝd{±8Ù4ñyáÆ6ïâXËåé}2:¾÷¼p¥a[ôkœ@@‹ˆ“í¦y|@4 V‘XV7Ëij÷~6nÆÕ…·½að…ì9¯«•k†I}Ã1®!VÉNÕö¨•žm* dë“LJZJ¼Y°[í–H ´7ï´ŽãEË_KŒÒ9×ø?G~¿Vè ƒtçh‚â4pã0$Ll0»xº3ðªû¶c°;|râ5÷ãé[ì)KeâÒ1D%OVèÈN= ˜)¾ÈÝf;{¥†ôŒ×N’Ç»$NVd!¹-IÉõ2Âê† u–³áD‘h¸×¦ )Ê•íÊígãv±³úRoì[L]wŸÿë7¦>¯§t÷ø@OÖ®PÕhm¡¿|Êÿ¼äDÍ¢OßÖùßAèÝQ:çRÒ3åôk…®08bƒÏÔ ïA–‡Áà1¶ðì"d€D°Á®‡Á”l¨&í?ºÛ¦‰é[lfäW)}q×8K?ÌË»ѧ*SŸ§¥`Ôõ­l×ËHàODÃó‰ÿ–¨k‘ò“5+\)#š‹H6y:ºL„r•+¹r¢RQÂW¿ò5]4®ß4¼B,¨^„çM¶ûÙ¸•ìÌî ƒ}¶ѳ=ÖÃÇ Ô7œz²ŶÙ%šèû @z#§ÁЇ5—n²=Jçô;ÏèOûµ%¯ÙÐËóë­ðè üŸC· ƒÁBš´Jœ>—㖙ܧ›@ £@Q|¿&/_úmë;Ü)ðÀìÁFd‘®Rþ³ç°¦dº8¡L"ýðu«ð%‚jb»‰8ïÆðÏñtÊaç”[ƒ9™“–®f•2òÖÓ'ðvhðt`Å4NQr"ht»R0Œ«9ÒY¦Q5ÞB?€œòûÙ81Ôooì{ƒñ,14ÒÆÄóÈòG+R×÷7©¨¾áTòlŒ,G«i¾•¾Ó„Ÿ°§¸ýIz¿'C?Jç4UŸ2ѯ(Ù1£>š­[OÙ4!Ô™4p fìØéRŽŠê s5Ј©°p9NhÄ& æCJHîßÉ(˜¯Ì5žåWÄ¢á¥Ô‘“9£O08sÖ”L æÚÒk@ 'sVxÛMÄT”ìøÇªšOÎUõæab3u[ôÁVÊH ðÀì!\iX±Ôv±ö<óBNT‡EÈ‘BœpbýyA˜O=a꺈³4K÷³q³Š.¼í ƒ}öq(ÓF“ä8oÕ÷7RßpêT6_º_+Ô[áxÓ Ÿ¯¥B¢¡5p ù¦:^Äú˜êç WëŒss>1/Ÿrà 1Oq§ jœÉÜbÑfí@ÊËTZ>Õe~«ÂqpQ2r­|$&+Ùn"†™§pŽh>ÂNäv˶sVkÒ2ô3ÊJí-ô¬å9k 1âµÄ!œÐÐ aZ‡ð hô’×Î2eiqz bKò§Ët?·¬ë’œÞ0˜îá°GO`Y—¬£Æ”UZßpêÉLz—¾Aqø¯¤ï=gë-è¶‘n”ÎixÊD¿V¨·Â;`0cÖï]§l¬j\ ƒéù V‚ ).Üš3ÍTT3ÙYÊ« üÐmв›þ ­30÷­C“sxÝ¿”lÙtÊV²¿2ß<Í„õKY Äœ‡x¼–óìu?QS¸•à£Où±“tÏmIj¯W&Šà“w›d„ØöS£¿}_Y{Nv'Ÿþžš™Ârò*–i›ü-±¤±œ~6Ϊ8$ÑûL²/À Îi¨Û:ö¶©¿Q¸µ—%ò󴆪0i*ÊŸ.Ó5ô½ Æ³¨ÖÚŒ©ÍIf”ιTø™rúµBvì¬ÿhŸ>SÃ…,citd­ f¨— ;ˆ5ç0f”ZìõÍÈZ·›.‘ŠØàAûÛ=³CøB?°^ÇÓkjÄ´1X¥‚Ö$¸ÕÓÍ:å°)¢ïM$tÏf gÖÃ`m&oót*·Æü2¸ÂÞ%¯Î—ézßâ4S`'ýˆ—Rû9``T‡‹‰‹hômÝú¿&¶[a5§ÊSàèqM™ØžÎ‹60LëS….¯K¼ý¤ŸÛ®»…¢7 ®á¥©ÿP`kªáA4ôsEŒ´¼Ùíy¥•¾ž¾t£+Ò?‰ýà·aõ`ÞÂÌ£tÎzuHÙ¯6­°cd7Õ[á%x ljHÌúµ@ˆü­ÍÈ Âê©„'ó7EµYÚ†õÒøB¸žû¾ôƒlù´zðÒaz¸r‰fé&bÞmâ¤ñL ÿV‘·ƬcÍœãAšØ®T Ë%ü`âÄÎs “ºæ+ W ô úF:©ïí{ÝþAxƒÿæû?{ÈU9`›úm×ÔÊNo™=Rgc½¯™³%ØŒ˜ÛVúe N¸®žá @Öì|±ˆƒfs£Ò~Ì‘(Í4Я6­°cd71¨g‚ÄmhàN46ÐgqØ cþY\®Í2ó‰åå ÃJ«*ód.ÙˆœÉYZgVP8‡9`ø| ÕDÜÄI?♼›·éKœW¯ñG‰RG0åÛy Mlób¥ÓW?oÞBä#]ãØ¯,\üœ¶ì?~HV¼zME¸¾q¾Ñ r÷¥ûÙ¸}ü¬½uuopkÿA¦V^|™¯½¦ Êgº#A¯€½%¥rZé×ÊYæ«j[‡JEk×Õ™Á˜<á™kYfž3JçÌy>_º_+P²cF/|Ti…Ï×^!Qh€Î¯i6!‡õ; ¨£r`V@85ãÇf7á¾]ÀÜèóp^$Z—ïË&â&Nú;â¬=BcivŠ›M \?V¢‰mª«T Åʦ¿\¼{`SRÔ“øí{n”{ú]SK}~?WÏC eo¼©OšF+BUv¡ñg4pÂŒÁ^08m=¬w{½ØJ?«nív¶%„ÌçD'®ÈÁîë8&±µ’•?Jçô¥ýi¿VØa…+M0dúðmÀ°ú¿óV+½SÔì<°×Ÿ}$MèÎŒ ¿E@ß͇½2Jç4†O™è× õVر¶kê­ð)[-„ lj ~ʃ‘ Èç×Úè#¿u²·È…uØ<S®C*9ib»Š¸øÉL1óYNXÅ«Âå•}~²ñ¥Œ¥¬<¦pü]Gýc¡T#•JüÖWÝÏÆÕóPCÙ£½6ŒæÀþ³¯áè‚”`Këé#Ê•UôűVÌ|®%¡ñ7ï„~ýEèó ÿÒ9D´_+Ô[aÇÚ®= ¨ì`94ðr¨€D'üæÝ,Ýýlœ[móÃÞ0¸†¡NM¼¯á– +x™¿–3£ß7Yî%þñß¾­õÌ56óG霛‚ MЯê­°cm×µ:£†n£`>4°Cõ=G`0‡S­ ·e~ý$¸B››ú®ŠÛ²\ø ncÂøDB—}¸W,¼‰íJâOûæ¾÷¯~åk@_~•@ùE”YYøJgºN],‘•¤|‡rŠà–X8žâ!4§8'N»ÿúÙ8·Ú懽a°ï îÚÄûn©Á§ø–Vrfôû ]ÑŽ.a¨ÒKWjÛ“=JçÜ#Û8ïôk…z+¼4²›95±]O Á­Ä/ÚÎáeâÂNÖį/¼UƵ‹ù] —Œ: "¿KN˜öQ]Z¿<ûÒù›,ŽöTB?·äð’œÞ0Øç­kïk¸%Ãú`m™¿–3£ß1)¹¦éL?÷ÌŠ5NÖòGéœküŸ#¿_+Ô[áM›»$¨´Âçh£"4°C•ô›ü´¯^ã‚ãúüZŽ;Ë©€¶)©ƒ‚ŠŠg‡€õ¯`þ>ùÐFéäzõzíà£&¶›ˆÅ3Us0²Òé[³uÃZ_x«ŒõÚƒ²ká’QŽ;*²%±ä4…®äE·_r–”yN?—×ryº7 .ªÔØîÚÄûÎx³Ä ÖZþZb¾~ÎÖ¡TÄ»ôص[óG霭rE߯*­°Ö¦Vx,=·¡Ö@ýLŽ Ï?ŽÓ­3$+a0¶•¢6D#°ëÍ×ÖÜú渟–p$bŽU¾ÏIÛMĪ%°úP:-CÖuÒZx½Œ;ôÜ©pɨÃôøó,ÄX4Ù£ùx$/º~Óùƒ+þÄF¥ÅìÔÄûnÙÖ`í’R9kôõp¶Õ¢•»Vck~?ÖÊÉ=Ó÷k…z+ìXÛµG•Vøž[6d¿s 4 @pX^Î1Âõb1:\ŒeÀ01×j8±?ΛÂPß¾w8ib»‰˜JE¯ÏÁÐ Ë9©—Ñ©´ø¨IÅÖ2Ùk&J$íJL×Ú7ƒô:@òSh÷T(Åÿ»„Põ³qkâìË¿®7Xâ òârÆ–¯4žµ)ã².?g­pÿ­Ê§®Àiܼk+šŒŽÄªaúæ•ö³qy-—§{Ãà;5ñ¾†[2l¾Ùå£bÎ=bÖ@Û,Ã-@Úæb­™£tÎV¹Æ¢ï× MV¸hjÌz+n/€K`Fl…³åM¬(kÍða‚ ÕCFÀî§ožÖÔµäd’üUâǸMn´/¿Làp Æ'õ>‹)0 ü€ÿ" V[¤† ˆbZ¡8­²]¤^Ïd‘¢^º ÖGë´UOˆyúÛˆöê5e¦ò_½fEà¼ßÊF?ç0¹ãQolƒÅá­^·Z·êàkº®î í)V)SA¡„Ã+}SÕN#TŒOÚ—‘§èôžãšŒÒ9Nð¨_+Ô[ᥑÝÌa :ƒâí"„.Ô@ýu0{–`^_nåœdæ'½‡Ã`¶6Lh¨>˜Ââà**áÏS9õ %|‰ýÌ&ˆ¸FŒ[ŒÚ±•ú3UðæWJh+b²½ ²•˜üRBó…hC–ߦ°X±±Æ¶Ïäì)‘púVHQ—dÎÈöÝÒÍ€Öz—„£Àlô³qû„]{k®þ¦âPˆ" v­^ËŸéVÝf­‰ƒ|,Fà—þIÖ©àVÚ,1+Üï?ÓË,DEÔBßs e+}Ó¤j.Ökú»-`ò™hù-” ´O+Vº1Øa›G霹ŒçK÷k…z+¼4²›9ZáóµcH˜i ~&Oìǃ-iA_æp¡;¦ôåx¼pÊ&æ ãDM~Ô)úGJ:²~òØä4–Æ2•‰úõÿ…½¸L,9YÒXÎ1Hn9”8ùÏ'£i¯˜Vñ Àn!ßýSÏñ˜Ôäejü·ÆvS1B0€°ÀuH Rj*d˜¢ì å€ÔÄF?·&ξ|ƒÑ£’_èú™5¶©‰ƒ”IÄ,*CK°yýô•¦Â…À t6*BŸ~gk¥¯€ªœomê@6 ÑýÝü <ãsöÇÈ(ÓÄ?e¢_+Ô[ᥑÝÌaž²9B¨ÐÀQ¨€ò§I»tî–ã±Æª:‚<™­Œ‚AÝDÖá?Áv€ ñ½`PŠÿ žÍ2²Eb?sɉC¿FŒnqˆaþä2r ázÌ€€ |x®-ýÎ^$m°0‘Ó›u ïÎh6oרÞ|1''°ÁߤŠ c9Á%iŠbE Md­€ÖJÛÁF?·Æä¾|ÿËÿógòáÙŠ‡gã¥Èa“n5ÕóéóÚÊ¡„bÉd6®EÝžZ”^G!s­ðVúú¨%äSÈÓÇZŒ¬±A~ÉÌc°Í$Æœàÿ¥súRŒþ´_+PòÒz•ÃèpFÜèü‡.×@ýäS8\‘Å‹GÅ1{á\³äàœ'Ú¯ôG7È+ƒ X(EA-Þ¡´%'N!MÄN9IÒ ;”<ÂEŒNÌõÍ»>ýòéQl§]ï)8§7`˜¾±¬k_kJ¦A¹0w¾- 1dôÌ6úÙ¸%o—äø0g/H˜ˆzNê?ÏX‘%Õf¼Då€5Ýn6±¬üÀ•R[á› Çp&6†nºVŒ?º[é“öê`;S1’ÿ¬þïÈKÉLMÁ°?‰©Q:§#ò õk…z+\´³~&c°¦ B„ÐÀ> t€•Vus—ü)ÈX%÷/¾ÇóÃ[æƒÂ°nªå9´àS޾z«öj.Ù΋ªOkç´žÞ(eîí¶¸XÆB™ÏYø¸Ò @bnŸŸð? ùð‹£Åé'À ~!¦Q|ú~6ί·õé& ¶°ð0KÃ@/{ZL0`+ùÉuëYõ„J°§ª[NoÁƒÃÆR¨ú¦HCL.DæâÝe¥–C‹Ô8~”Îi Ÿ2ѯ(Ù‡²—<½Ð Ÿ²)C¨Ð@®ëÀÒ ÂN˜* Êò†’ Pâ"¸Hp»¤±œ&bÞ*à4›•¹Æ‰ÕB‚W|¶sâÃÓ ðõbe±»e¬)ã‘váÚžQ¨e½“ˆ§,úûÎzÊ·M·î¿~6έ¶ùa= èÒ“iƒÁ$Šè×2ë-fúTvje?Öš —Ó_Õ6ÜsãÖ–ÜJŸ—[—®Çá``­åU°¯hFéœuz•ª_+ô¶Â£j<ø ¼ˆz@ß4èÌ„™ ›¼é“±iÛ=íü*^tVDvÛDÌ{ ŠE#ž•ý%&Û3X‹wó§/“‡páôSÔ4&•˸*òÐ$c±'“±ò„gàK[Õëd_'¡^µª ±ÉF?çèjÇ£ ô}Š*Š@|¾#Ã9lˆ·˜¨ô+ö•&VûÒ@²ìuý®µäVúíòi ÞþøÁ€ù¬X3oŒÒ9wèm WúµÂu­ð@M¬†zhຽž„O¦sõí7}WµŽ°/5p#C–vZ§=\Ü:Í>0¼äÖ'NLÿ § ‡˜GT-«J4)×|ýøA_¸P,l¬(€KÀ6~{æ20”Oà‡œ{ÏO ÿS5[ÞpK`0 Å.ìµ6íµºD'uE>SMÚFϺüN‚Ò,Xúù}ïÿ~6ΫµýY¥7XC  KÏ+A¦!\k1[i)šL,‘8°ášJn夵'kuÀ%ûКJF+³™å£tÎ"ó§Éì× ”Ì?†6¿ÌZǦ+×¶§i¦$4ЪFœ¿–´§ Rb>‰;UŸ§qËeËD­UýòKŒ5»Øì´r‘àÖDö”ý´ç»ö 1–ÿ!>[Ùb§dAƒNôçwÙ¬äõäøZùÖ² bØæÝ"q‚Áoß$·Ð`"1߾ˈr¨ší8£Þ]á"eS, ð í¢„S~“ŒN¥ÅGMœKXËT'A“l@;„ÈL­Mà„«Ææ.¾VïMåWÂ`ýñ84@— M—#Áµôç9•³²!Lo•£{GÃU–Üʉè›z2ŒžÆÅ4¨IÉ€ ®BüæFkqÞ0¶GéœÆð)ƒ¶l;]ñ”-B…š4ÀYÂ×b&R–TOåá$§H¬ÌJœ>Î>3O‘œ¯^Û¹šKqxJèC› ”èGÖñCƒEã"í'TÁ§+º];MHÕ"“Åv‘+2¸ÙаÙR4Ëч38‹ì[3ð­=%Ä'™øýhBámêk’qVÑæm'›¥å*™@J-‹Ú&.šØr¢a‰L&m /m™ÅÆUÂ`ÀmZ$NМ¾š|õ:?D"G¿–®°õ !%[Ó­3º[®¾äVN Wá$jÆdš^PršQù*Ó]>CÌR ÈéÇ.Vg¿£tÎÛ'»´eûd'Ĺe ¤Iûù4?‘¾‰{û£ “"x1ÙÙ·ï·j¬*ÊÁ`5,Z@ð²ˆp ~2OÏу§Hƒõxi  |.<0¾WP& 2Y16+pY6<PÉÇ™cšl¡òV|Êù=Ü’~r͹Þ]ˆ±’É­4)Yþ¥%'Ê1‚ý€ÁdÂÉša­”q­.?¿‰¿¨ÙS±‘‘: 1#³[úžTAޏZSˆ^ÅXÔÃ`À­ÎOúÝÄÀУ4SàZ¢µ!¬K<øø@礵ò뮵ä&z#®S†™Yq0O"š3U²–LiÞ(M2¹~Féœ9ÏçKÚ °]´Jçk (4°OŒÇ΀…•?»s[ ƒá˜‡Ã*!áÇ-¸]¹äó”ÍnÀ$W"vÃ}yÊ…{0V4¥E¥Âß¾ÇW%û›$°kKz‡tÍSh€Ä\kļŽŸ€ž2ao0üàÏtþɰ~ñÅ4@>ˆq#¯Ñ'¶§@b*‚ïüóÄ­§À ×êòó›8ñ‹Z>Eu´8:§Š»ŽÈ°rV(VË(6®£(…ˆç¿dšã·˜¨°yC$ݺ †!xÝ?$¿ôú膸©ášJn⤩'?aæ`^#Ý€ù:”nÌ€eÁký°˜¥s™?Mæ ­0(Û§é6!Èík€1²„¯GåTZU´Äæ  ‹ÀE‚[GuİÓM¨­.,ŽCÌSûï):×%¦64Á¨PRÅÚy_ S˜äJq}òÖêw¥ð§’élUÊ_¨¹É O ðëðçyb@2 ãbKˆÌÒ52Î^©¼m夲X‘©“Ђ,CÌí¶V‚¢@2ñ—†fcQ ƒÓV´è›ýѯe2`×ô™çÏ‚fþt™nÝŠ ¢œÍ†k*™ëé›zr­Ó7iË µ¿yW<0MšIÄÓÐeš@Væ Óä(Ó>ebÐV€mºÜ)[$„ ¢ÆÈQ wYN= Flà Vó)—¬•CO6 àS+:ijG¼¸FOQ<å²l¯£d‰ ±=«¨x+°*`U1E²Y¦¸še:·Ð¯±í¼U󨕓š2Ѷ”KºòÄ›Š6ª,íºd•0·dBY¯^³§Ÿ_†x‹‰ú›7„:¶¯Ý´…O™?­i8h4F*Kn¥·y&gl™fáÌ E- íÍΦ¡-éxesŽÒ9—j9SΠ­0(Ûgê9!Ëk€1²„¯GåÔ[Ul ;¶r{’àÖÑ›c|¤ÏqÂÅWøÛÂw‹Ÿ2åP‘xüéÝŠ”؆g®M)z.¢# ær>‘«¨ü™¤³ŒÏÕŒ÷ÿ(Æ¢ãG×Äçx¸ÒL=SmLÿ¤?ûí=ëÉ~|¾_ÔìiÃèžÞl¢‡mÆÒ)ËŸgfŒ{;Jç9(’p]:h ‘øH׬0£)|WÑz¤¦]à@ÕÃq»ÄcÌêTß,svkx@¶~ö4¿•ŒÉGÇ6è–Œ¼ˆ\@_½¥Ã%¬®¼XKÛÓ5NvËhUT&69©,gIÖ¯äQl\% –+8‡¸5éÊKáé,îÇ00 n—-•ç´öäüݵtÓè¦Vz^ÑÐcžáª°k¬^ž?Jç¼\Ò[.aÐV”í[î ÁÛÉ4Àñ-8C5,^¸ü£¹YQ•Vlƒ%µ?Ešâ÷Ü㆖Ęãe»¤oâ¦Þô«ØÝuÌ çÉš’õGʦ_}Zµ,Ùrôed0€–œð]ÉÈ6(»Õ€[¾Œ¢×·xl( ;ß××p²OF¶2QÃIeQ3²~%SÑ(Æ¢ ³²c„æ—†+½ÁZ”}õ+_ãÜ®4pÖÿµöäõ’>{Ò4ºy³•^l3©ÖÍI)‘1iL›8ھы‡üŽÒ9öf ´`;¼Á7Û©‚±[Ðcd†]g·OŸw•>·ÁüqÍèóÛJ¬ï¬ ã))Ön?ÓÒÇŒhžâØTþÿê;ÜRÂgdÓ ”\ÀN¾[Q<¹F,À‰eŸ]Ð/K¶aªÐëk…C/KBç}‘€¸,ãTºè¥ ˜ '1=,üÔp²OÆBenV 'n«û•L•£Ø¸JL8lymÂ`§YðaiI€+ÄSöh™híÉËŠ9TÍU9º)¡•^l†U»P±3`uæUL€R{qR*ʲ™9JçÜdh‚A[aP¶‡î*ÁüX`ŒäÀu™Îa0ŸçcwEXò«kùŠåTÂ`,† "£“ÎÂ*÷Åö+Þ-h`#149lñtŽð x.–Ìëð€YŸ]r9…óQ ž1È0y$œO]$#h– ¼Á'ªNô:Ô¼àç 9Ù'£SiñQ½NН;™ýJ¦ÒQŒE% î‚„—×& vô¯G„æ&ýÞ÷þ‚1å¼ÕÚ“¢–*G·½XO/¶í³MJ²Õ/­ Þ¼#¾Ë*½<1Jç¼\Ò[.aÐV€m:ó-+6x \WŒC­Å± O“s $ž8<Ã:ßRf% FXR.L¼ÒÅ#˜Ì™©P¹mSÚµÂ`u ·HÁ~['Õçͱiâ!¶Æy yN$&ÊÙ< Š…úž”?ô›—¶L–XÒ,sjd\¾åç–ðéëŸZ–¨×§ÅÆÕÃ`/»óvn°€õ´fÀÊMjø†à _·­=Ù/-Ú:º›è5`Q #—Û¼êY¾``ò9üáØþ9Jçœéäd·ƒ¶Â lŸ¬ó„8·¬ƈƒcí”ytšŽÒº¢Í2QcU¥œKSˆµT–eæ­å6Ùº'‰åöCf^¬eùËœ¿|k-G¥ÉV‰„]#V>¯`R¹´ ÀûÅæO•1/y¬ô(Æ¢365HíTìƒaìf«ÑaØõhG”©žÌï±=™á|tk_ÉYbÞòÄðlôÈßÍÉ^ =Jç|U\±ŠA[¶¯Øu¯Ø^Quh RŒ‘%|]惕‰[‰CØlë’Ørêa°q‹ýZ3aF3K€üÇi­ ÔÖ>}Aó‰xvûéÁ§Tâäã>r'qàTÖQÆO¼–ÅÆUÂ`¡:4Ø–®›0øÀnf=€¬Ïǔӣ «‹õÄJå9ûÒ·3LFéœûô<Ê[ƒ¶Â lÒ+‚Ïh€1b¨u-AÔ.áX–Oþ¥éì<Ãø…ÖÞ"¿'Cùø@ĬŽ&ÜÂÓm 2yVðÞö‘±ƒ‰±ƒþ¡=ä¢L"„Ip»QðÇh2.rÒOÆ ‰nøñ(Æ¢k•šŸ¬ºŸƒ„k¼Á;ÚÙƒø|Ðo¯æç<Á‹°Ÿçí¹ë=LÐÓS^z_3£tÎ=ZçA[¶{/<ÇiÃà44PÐcÄÁ±<Ê?‘Ã.à\âû8Þâ;/®£bƒAz”‰ ‹˜67©È± Û'Û‡‡¶ìáAá°TÐÚ®,„J—NoÛ i¦†äÐ{ûž¨.¶e䤟Œ»s/Ñýn‚-&*a°¾ØÊoêN¯^ç9Ëtýºu‹Í§çZîQ¯–r,¥Ycøç3Šlx0øñ夿-½‹%(³ß0A¨"iã÷õuƒTF霎2OðhÐV”ít˜a 0F|ŒÙ}ÂoŠÃ˜bÑòËy½ÒªêX°ô…õtô™¬˜]Ó$н€ óŠO×™ÜÍå‰~K—K (C}>Ã/FÖ)Öt"qB¦óJý£~2Öópk”£‹MÌÐßꌺœÎ°ÕG^±²„¾yÎáÞ`Åü³vfçˆ4}ž\ù¨ï¾áÀ`ýq=¶·tì œ8uõ&Ô‹*˜¸ð$hhçu-Y¥s.9?SΠ­Û]‡Û™š8d¹O 0FË#|¿L×Ée¡ËÒÏ çõJ¬oÏu0æ;^üLàCŒ%M±ŽÕnÕ¦–ÅEchS!ë9D°Ö"$Ó÷‰ í£1ïÜ$ˆ;ɸƒ“ÛyeçÃ`@o­ëWz—éÊÛÚjk‡ £òÛZH=ýÓX+½ G9Ìi:v†t‰ê)¯Ó01öX•H-°á+d”Îé(óm…AÙ>A‡ FÑcÄÁ±<Â^pà˜s9¯WZÕù¦Á0˜~qÚ,™¾†S‚_.ùR–Ä»s¨Oš|h8mŽ E€Up;¥©d9ë|&¥ ýõçDïˆá5{ÚIÆY-cÝŽb,|œþjÆÛ÷DD¤ ˆ)a¿J,¡ožÃ€íÑjùi½Lt¿µX™Ž7˜é‚ƒh8­‚ñHÂ?ï¢Ó0ybïãÔÂÅ–Áñ¿LŒÒ9—œŸ)gÐV€m¿w©B–ÐÀ 0F;{„Ÿ–ýD€1¿àUÀêQ±Á&Ø(äJg²UÊ|>D0`±ÁMœl*Ÿß¼ðMÁ-\x®(}“½Y{N³q¬Œy-c¥G±q> Î1íŽt庵µeéùàO½Usbvkù3zÏ(ýÛNÃ$­ˆ§`é§¶W¯™ú|NF霾£?´e{ôÞü¤ÆÈ ë®Ýb™½¹ÀÀJh2_£'ŸUÅL`€æqt¡ÿG\ŽËèØÂ­d¾ôKÆõ­@bÈHäRû/V>5NŽ’‘oŽ<΢RŠ£ÈF1›0˜0`–«l:ÐÖƒúõ±q'opÞF@â6¯"ÁàC—Æ~àT ùáÏ fm[£Q:gÞ çKÚ °M—;_s„D¡£4Àqp¬=¬/ÍÙl%¤ œ¼ˆÓßÑà+#›%êa0ÐÇr¥P8¢S`íôÇ”õÍuå‹5d€CÀUp‘XŸEÈÇþ¦SE!&GŸQ2AŠvXÒü ¤³_Ž’K™¥û8©‘±T›—G #a$I Þ-¯”Ò³2–ŠñòF±q> ¦Y›ŒP R[®Zbk1_€ˆPƒ-½Þ0=c5a߀޵øTNvŠãfÉÀ$s sY-k/ŽÒ9×ø?Gþ ­0(Ûçè3!Å`ŒÌ°ëÚ­,)xXa‡ØFæ…0[À…Õf·TFaÓ.P)p‹‹¿§ŒÛ‡ÛUͧ*3ü7ï¸-Ÿ¹Îô¡_‘X™ò¢ãèN'½y·fUÑ0!šd|sòà ßÁI¥ŒÅêœÌ§³õÞ¼ƒgðÍzÞ!£ÃgñÑ(ÆÂ‡ÁÄó¿hŒÄòÚ„ÁEåÜl&ƒÈ0äág'2 ùH6õáÇ}-ëè>lN»!k@xÆ9Oùšj˜œ’y4Jçô¥ýé ­Ûã®mGï3Áÿ`Œ¬áÞY~š´Ÿ#"@­ÌùÜ’˜‘å·Øâͨóž, ë@D©”JñEkw•_èR5æ N(„¼O…¯œ` äƒ&™¼É)Í/¸AìpÂS¾4›¡ÜâbZÒË—…;‹åb’àrÄlå¤^Æ%o~!Ž:ŽßtšÇõëüNá­2:E­=ÅÆù0ØG¹›Okìš_2´É"K=™q§1¸9[9ÔÙn,H‰ií¿5º…iÁÃìõäžäe½iEebÐV”íSv¡ê65ÀÉ«“ÖYCÌØùÅœ÷rí­« êãRòÁˆÒ:ü¿¢Ædûp½ÚÓïþÀ $6²š„yt ?›×e…Èó‰‰Ì/ˆÀÉÀ}ü ¢ˆmPþæh3WÎ.+3OÔs¢·êeÌk©IÃsöù˜_]ÙŸóvþß*ãŽjF1•0$FÎÒÙå#aìÕ½ü+š‹ôy,xÒÆ`qîf/­ôß¼cJÞ<‘†Í#Öª\DÚ«Ûç ˜ÑІRŒ9“Œq>Jç4†O™´`ûòø” B…¤ÆÈˆå'T“ü¼£7#Ëok`°Øu”+bGÖF®>Ç‚Go*N! øQ­)“„[‰ `°Ã Üb‹åªÕ¯¼KNtP?H'v:~mòNð/-‰-ÇÄ$D~°ÈkbÖs¢Âëe4f*p’¶wà–hÀ gnrÀE«Œ• çd£Ø¸JLg›ýnÂàµq—ëêêiíÀj>ÓŽIiº[Uë½ÛŠ…øø!E;¼yç|vªsƒMÏÎ$cœÒ9áS&m…AÙ>e ¡nSŒ‘¸úi¿³„ su^©‡Á”c)rõí{G]iqúÛ5›•N9ÅG)pwòx#¬s¸¾ë™ë,yYKáè‡|0aâùÙs•Ò[&ûIÌIR‡¾ž“·RF£¯LVÁê\4_êÇà±CÆJžlcQ ƒÓ šb–ˆίMl .±T[DÒ¦‰…ô€ZdKÁt{âáù%”ÂïùZᦒ´Š'0Ìgm”ÎéK1úÓA[¶mÍ5zÿ¡`Œ88vö_þL.f{~ÙxÌnëa0¢ÙPA™fMdˆu˜›•Ø5²ù²w¸1)Nø-Bí\ù?^,š?ÈrŸ•ÒÉsUÂÌy¤%£J^³ž+|MÆ<¤Áˆë€Um S~ý[5”;d¬)6§ÅÆUÂ`†'0ؽ˧M6×Þ-¤éuþA‹MLª´Í‰ÈÊLkçÉ \ÓùÓÓw¾ü2®éÞþ¿Q:§/ÅèOm…AÙ½·ÿi€12îk·Øß4Õ~á/]£'·U­Âc"«±;½ZdvJÒìÖ­uÓöÍÞÞÐɬêÙí¬¬Òíö‘P¥·ò<«‰Uàªøj>/a#=jv»ñòÆãQŒE% Ö8Å?¹ÄºNÎlƒcCe·ñ˜Î†O•ËïlŠ,je™ÂÓá?lŒ¾éܬ-™—•2e°q/ó[3}Ò9—’ž)gÐV€íš>v¦– YBM`Œ88Ö! ÿ’ö»q“ê¯am/<0í‰Û Ò`qu¤°€‡GZ‘eïG0ÀææføîÓ+-:i㤤…,–ž4æ}ü@`°Z‡ß„"úw¹Œ#£Ø¸J¬“ žÖJ8˜G»×­Žbû>š:›0?‚uת#b2&F«˜­º®v¾(™.í÷ä|'ˆŒˆ•³LÚ¡Ià„1εµZ¥s.%=SΠ­0(Ûgê9!Ëk€1bX×OÈÖäçBÈÔƒ14öaµ¢ò½Œ¡ýGÐ8\)~»Ãñ Î[•0OØ;~¡O†ÏµPÒI ÜæÖ©¥IFÊi¢oâ¤È¤B‹š2Aìó¢b$HpÛôºC|¹ŒNᣋJL<𓺦ÍzKoÂ`GE7øˆÝ(Åá€Bél Ü%« ¿}O°£2¦ F·ï%K ÿçó¶{r6QlF)Ó:ÁN¸¨hm 2Jç\ªýL9ƒ¶lûýüLm²„vh€1â£_{šÀÞ«×|z£â„™Àɹƒ`ñëÊÉñ8sðÕI±Ž,<‹€(;>Š'g:ïn>J†õÕkÀÆ_<ÈH9¯@ɇlL2\âbhb«Œ­ôb¯†GƒÕ”¥ƒ— “€12ýª+Ÿ^.£SÑ(6®Ü+m {´LŒå :Ò%,·¶šžµ5Ñø¢dœ²|ffò™‘Ù­zò¬p§'3WQÅSt‚Bh#`pB&ëH£¢°O$‡Èø©¸EjW ƒY‘1Fèùtd7‡h€u»9`j»N†–¨È¨©¹¸WCEž%3¯ ¹‹Ñ­Y¤'E¥¦}b-)Ñ'õÁdÈÅ~Þ]9x—Ä–C± `?>È?¥yÈIŒÒ9sžÏ—´eû|ý'$ºY 0FfØÕ¹Õpº¯9¥í³ª)²ÎÅNX ¶uaw”Àô¼¼’ÉÆllš3ãpSF£Tb“~7'VÑ!Þ`JK`ŠŸ¡Kpn[&.—Ña`cQ ƒS\ë„qËÛ>™Š&±ƒÝÎ#–R›VâiÉ®u<©Oäx…«Eî¦8*–w(y±8ÃØ@¬Ñt©Ì4Z×9I¥½zM°‚–>1ô£tN‰ÖßA[¶éœgm”+4p¹#>š…€éšI{ír^o…Áì¡s1f±SÎ?Ù‡à*`›˜C§jD³§›2BÙJŸîs"JiÛÞJ0Ø]}¥Ÿ Xœlrµ\)Ö§ß÷tSÛ­ÅŽbã*a°0°Å ý’ úMènåHáÖÛªäéécô.K0i03¬UAûÚ¼1ëùk¯¨pû~ãY ¼XÄÌF'\Úu‚íÍÑ:Jç4O™´eû”](„ºM 0FË#dR×~×ë­*¶†Zöc`··©4ãŠï-JÙ2×H—N7ŽH"Áí¥ò›è›8‘¶Q5U°Û+mc»ýՇϭ=mbÛÞªI4ÉXS`N3б¨„Á:Å%wù2~É$‡õ¬ƒsœ/mó ½” ‡[GÆ&âOå|ü@èE}<¼jùôz)5Jç,ñ~ž¼A[¶™ÛÏÓ !Ihàh 0FË#¢ pVpik•ø7ÝÚ¯óz= N!‹Ó~%NÜú‚šý"aiÿ•cŸÔ§ÐD’æòÑ»ÖD>ýaewÛn+éwpž”¶iD@‚•SÉvS;dl*âQl\% ¶àÀž˜´ }Oì¤s„léúÛªÞ~ô6ük¦Í3©‹NäúóL±$l %KÕÆØRö4W<ÿûÁŸ¥SŸïÊÿÒ9ËÜŸ%wÐV”í³ôšc 0F›?’Éÿ&.§']iU±ÛFLÁ¥¯^;FDŸÃ`qp¹àÒLþÌÿ§óÀéRhâž$P–¿9BœNW~yÑ纒~'¨—Õj—¶ëW>Ï<­d{³œœ`‡Œùë5éQŒE% Öh¥¡‹—KV‚[£«Û¡iš 4Ï0ôtš_ƒëóL1 abcˆÎÏÒR%;_ìR/œky"Øì«t”ÎéK1úÓA[¶ÃXE(¢ˆ Æõ 1:tØn¥¯áäÏ¿óûTŠÉÆg…·PQÊDDв'MZÙn*¼FƦsâQŒE% ž¹y+o‡ó7MšgØE" ìØ&bafÆ5cœa…g¤÷®Y:[ñæ>»3¼=£ÉoGéœ9ÏçKÚ °í¬ÈÎ×L!Qh UŒ‘v-Þâè`]ߘ'WÆäÍP¢H¯ÌJ Ï)H•¿1?O‚[_×–ðé;=…O‹.H‘„oß;%·ÏAìÔšÂ!æQ} 'òV=E&¿yG¢2JÙçsö´‰íÙ»þmŒ~ ÎÓQl\o|Ýå4ÐÚ#cØk”äÓ…˜^¸˜ Ã)B¬ÕŸN/g¥IùÎ">yÊ`TÕ¼Ë<¥súRŒþtÐV”íÑ{Kð?#Ž=øå׌&¿­‡Á˜9v´QXcÑnAÉ9ŸXX o^ãM‡;ñË…¤kd–_O_à 4„%Ï®š(eã§2!¶U]˜•ÅÖÈXYÔ’lcÑ/5s¦ºÇ”qÄ7!L X~éÖˆÙOY¾ÅôE±:-ÅÛ¯¯ˆåþµÍ¯eQËœQ:ç’ó3å Ú °Ïœgj‘%4pˆ#9p-¦™±ÙIÌãótñeÖÃ`ý©¦OM¹éÓí §’MœÎ@ó‰ø$ÁãƒÌhÍÔ”¾IŸªn-õø@x'0À6¨õØpBá\¾ÉvSÕm2¶=Šë ƒm¯–xÚÙÄ2»­`ȇš“U5òˆµ¡yƒ+ê op’ºÓŒ2EÌ1(Û3)â64ÐO›0X}‰ DJÙË[^‡À`ŠåÓiÎoëëï~"X2ß—qT…é$¡JçßãÛ¦"æ#q‡Pš o"†OØÐî0‰ ¶7|ü€N¬ð†ú×$ck£‹Þ0¸UocÑÓÛ/œgÿp¡Ô $?>À Bš üG霾£?´`ÛY”Þ(Áhàr 0FË#NBÒè{ó<$ØÒÎëõÞ`¡&…ΪdO´Ç[r ~ùe‚p[FÄ+ê²gø…pì°Ê+‰ôÝÙú?–ˆÆžlŠž}þ$#ȧ:Å—š o"†O¸%‚k“í"oN¦8¡d¢/(àá7=j’±©dˆG±qõ0”ÅeÇÍn-?OÔØVõÞ}ÛM' Azh€1âàØüœ÷µ¼ršYºÒ¹$„cáÁŠ,u„Å5áÀM ª$Áå÷{$h ·:†”œ`%×jÄíCȇžbA¤yŰqþbSáMÄOÚ6úcrM_ŽT)c’èÍ;ûü‡u …2 7ɘk²2=Š«„Á3ÜKÓÐ\Kè›çTØJ•ÞYë$·á›w˜f‡óšG47B ,1µóMNïš|š&ý¢ŠOG1•0Xà6ÿKy:‡¾yúÜÞ`u¡úy¦ØO.÷«ÝU¾þ²y±.Ë¥sçL Ú °MÏ?e‹„P¡C4Àqp¬=Âc)lB¿:7øù×h–‰JŒ „’A¤œ.j9DºÞ…¤ ÍéeôÞ“ƒw­R„2MXã“ú›81•‹*ÖdYæ§à u ýiiû=®)›d\rèçŒbãš`pqkÒõÖWæÍ>½|ž¹£¦>#Õ×£¤‰ó56Jçô¥ýé ­0(Û£÷–à 0F–𵘣hC^~h0é"±2÷YU–®|q†qQâ;͕ɧ%雸[úG€i~±°þ¢èë,ÅÂã öSáŠ$\êÄÞªç„Wà¶¹HX —$ øÍðï‡y·ÖØ$cSᣋJŒ7^§úÝZ~ž8·7Øúƒˆ‹ŽªyÆòk »›>5…hÁ(ô[³¥sÖÈ>.Í ­ÛtõqÕœ‡zk€1âàØü^ .Üždê7ZLïƒÁ&²ÂAí–§·áÀä\µ+ž‘óCš8XŒO›U‘ß.ubO«8±`{íË/“>ûül[x•Œ»dÅÆUÂ`-Z°B¹,'5~sлL_8`w)~çKÅñfg£C„€'–$‚¯6¦‘Ç Lëî)¡ÂA,îœðŠÏ %° Yͼ1JçtÔr‚Gƒ¶Â lŸ Ã„£h€1RD°ËL™QÌ ÂE‚kIi9ZÕåþ£1Æ—Ʋ' RÂu/¦|}yD¨ß}£¾£j, Ö¶ÈŸÃ(‚5šâ‹–¹Ô‰Õp¢ƒ5¬(%\=£¬¼¥‡˜hßûÞ_ –Ê7ÉjdÜ,d`cQ ƒÓ'roßÏ/¬«[r–Ð7ÏÈ «3nkzr Ä}õš üÀL#ôÕµþ@>O5ÕÐ7’ÿÖýØ“>OÿäÒ(ƒ˜¡ºV8ĬSHÒä[`]£Tþ(Ó—bô§ƒ¶l‡7xô¾üwÕcÄP«“ø,6xšº5óë¼u! ^~”Í6¢ÎUã7!ÏÉÙ¹]U,ñµ³É<ó?÷kœÿ™t²¤— n‘ TrFµÿ[êDeÔpBKAöô7ïô—ï>~H_öTqm±*Á¾SZ:c*ÿ¨Âëel×èӣظJœƒÛúô…v·òw¼ÈŠ˜ƒ Ó*¸¥'Ó9‰ãRu)Ô|ý¤2 4ܶ¬èÄùÃ$â8Ae¾–%ÖÈ'ÖgqÐà ˆÃ†X¥sîhÇ^´e{ Ž¬Ž®ƈƒcóG a^ŸöõÓ”Èifé ­êÒó Êâ2cD° \×ZíRµýñ „å+Bx#ShLZBGm`»ÑÒn¶—:Qù5œ¤zß¼ûêW¾6»xטÜДÉý;}(gURx½Œ»ùÅXô†Á»øÂ/jcȺ™k-M?0Käij¸]óÙj ñ"Ÿ¾RÒ9Äñ”8Æ8 TC ®Öyã"Æ•í(p”Îéˆp‚Gƒ¶l_Ë>ž ÑC„{Ðcd†]ý[ü\ìâñ Øó‰‡ÁØ#êÅÄæ#Ul 5ߘtjJ°;§r„²Å‰$"ƒh‡‚æõʃ­OÞXMìûvl ×p‚™Æ»5»äËùÜ—¦X,;óm^þ¦­©®› ̉G±q½að(“©€þw6§'?…}ü¦Mêqr 3rA­yÈÓÂÌÌ6€U<½ìt80Xñ€aXbÒc‚Ú$¦.ˆ™B…Æ}µÒ9sž/=h+ ÊöùúOHt³`ŒøP6Šaz·KÀ/'˜¥{À`*M1uÓß/Æ^÷_âdò¯Êª_ßš±³D+ÿk0¸•«÷Øð]+V‰c ß-㌫âí(Æ¢7 .*ç3ùë9 pfÿ]9Ãy)§O®ã)†!+à³d‚ÁÓnWB×¥sb"~õç,‘ £²qöYqÙ†0ÁQ k`0o9Äzo”ΙIyÂä ­Û»ÍÍ [1D ,4À™a×â-î”ü–a`™€q‘^™Âàe,¶Û$¯ãB”+dˆ™Ü%Õ™/u"±ws I(c'ÝQ¸³(h­t·Œ5bãzÃàQ,&[\ù?8_}Å}´U±›U,oIàŽfxDçd‹Jö§øþnÆ[Î'iµæR"ÍHÉØäšQêv”ÎYdþ4™ƒ¶Â lŸ¦Û„ ·¯ƈƒcóGš· Çe'‘ËârЈÝ08Åò}ùôGârb\d_>¹êI˜¿ãdM'ÒÏKr’·H!M»äÿf·ù£ÆtWG1> Géø¾å/'«é.7ó¯6¶Ï5Èg½kv»Å‘V·Þþì9EÑ??ËZ¿èËò3Ë9éb”ι.ñž Ú °]ß3ÏÐN!Ch QŒ‘ë:iÁ`ˆ¯+Aæ108³_à=ÿ #0Áì<ê"4·QèãÈ»rr´NPìò8SÜ˳¿Qr¡vh6|©ˆc"@eÜ^Xà§×{j{çÃàt0Z¾e³HoÂà±,fsg«Sû‡ÉãƒÖi@e}êÀnjm£‡—F霮|Ã?´e{øîŒ£ƈ}óG@_,¬ÂƒIcŽepÁ˜3ûVÚñŠH¯ð +ˆ‘ ÁíµôÝÄ æÄÎ/Ü&è¾åÇ>\'´&M6ÓÕ¦¶gô›·O0ŒpJB+ÝðËÍ¢fMÚž½»y;бða0뎤ÿ·ïùüŠKm¡´~7að¦¢nŠ µ³ÕŒ©}ÄMátx‚"øÝXÈ—”¸öí(³$ÖyòmØkm{ž’ ¢ÆHŽu´Î·”ÝÁ!ÌÔ¤ ló^ùÆ/ú·!µrƒ`ÁñâE$“’×ô'bL†ŽbWb2×èûå910¿¬WÁÕÀ˜/¾xÀbr-iÈ9J'KNdßÑp~ 89Ù—IsÐUhw.B%Š nÕv+ó£Ø8ËLOÜ¥-¸|è›?ÝÅÔªí£è+;[Ó˜Ú7LÐ9KrEY¤¨ãöI)`ðQ½¢S9£L3ñe{&E܆úi c€7|Ô,_žXŽÿW•7­júTäÍ;›¼»Éy¸åB”÷Ÿ*Þ-Ñ$vXœCôÙʉ¼sè¥Á3W‘~:á7\U´È/iêJ ºÂI‘=?S`ÕÝ´Û¥òi«¶+‹5²QŒ…ƒµ¨a•º›Bn?QßÙšÆÔŽa¢YÈŽIÔ-=¶I‡ƒ›ÔõòÄ£L3ÍÀ¶ïŒšÑÇmhàÞ4° ƒÁløRœùo~{‰7˜á)ÿ‰üraM|`Z&ú[éŸ{wmÐ&N‹bp"# û<|Æ¡TA¦ürõÓ‰à匇ݷ°ŠNˆÜ¦¸øKy~S6UÔ¤í¦’!ÅÆù0XÞ`¢Gr 6Î]¾NzsÝÚªÕ®ôõ ÊóLμ?Lf€<-FÞ¼£Ò¼„ÍôS!%ºQ:g‰÷óä Ú ƒ²}ž~’ܼ#ŽåfW;øk¿Îëû¬ªþ™£9;É“ªøz⮌ ]9©×I+'ɾºŽÞËÕŒ·9ÅIçjîªíQŒ…ƒå FçÄžJ‚‹‘KZ—ƒyÄ€í:^/ü’ζ9¦rn}br}ì}°úCçlœå¯×¤¤=Jç¬s\šA[¶[Wdã¶QpØ¡ƈƒcy¤?Z*{Züu^o…Ál&r1fq™:ÿ Á·£ã†°ƒl&:Ľ‰x†“ÕÙæñžgœTF Tê„«8™11ùœ}m/ÞhÈ@!œ¡ÚðÂé>·JMÏG±q> ʦ°¢ ý7aðj®QâUij:[> ¡¯éù•ðiFX×›w;00šƒÖEŽÒ9‹ÌŸ&sÐV”íÓt›äö5Àqp,ð.‚d–W ·›¬­óz= ÆÖàEa3ŸN .-*þ¶©Î &Ám‘ì2q¾™aeOs–« àgÚó9lÒI%'ªQ–†ÅORæ¡ÿôE…[r“Œ­b,6a0@7E¡¿}Ïö1Üò«c"8àe·êíºôü±c:[=MS‡ ¯(ÈÇŸ” f®æ—t=KÌc¬ˆýÕÇ(³AêImØö{×€M,‡ŽÔcÄÁ±Î#âr„Á˜3¼(8CÀÛ$Öð¤$W½lõòGQ!ÞÄ“Gêë󲨚¿¯‡}R’Xã$ýñ»iÉ`ߦ¥O·`p«Nj8û¨—æã¯¸¢=8A“m†ûs%Ìî´/O8„düÁŸ%o0†Í([o+µÝZ¬èG±q508ǺZéÐ(yæZº~ݺOɾÅet* >G4Ðå#Uj×0¬œ:4@˜”¸)Ü:ü3¦äZ᛼1Ãáb‚…¸ò·´F霎È'x4h+ Êö :Lˆ0Š#Öu ƒ1üY:)-m/ºÇ aûÀ“!à{á u-m C‚Æñ³é\å"'8ˆ€÷Cƒ¡Ä‚; VB“N*9¡pL0%ãäç—/ï‘ȯÕ{a‚bµ)ŒŒ4(…ɤ/]Xr½Œ;*ÅXÔÃ`€j· ¸µ†~- Ø`þVÁ:^f:aÆ_>Ó+ê§ æ"õ%MJÎjQœÐEÁÌ ‰wÖ}éÓÑiýÎ,Ê`¡ü;Jçô¥ýé ­Ûá ½ïÿ]5Àq°®óè0üñƒC ·!Âþð¯¾Ãíš{Dæ)‡mßýA9 ®«ÞTxò|¾yG„ÍÃà ƒh€á ÞuBiõœ÷b£á¶ÓQSbƒ¥êÇ”fM ¬b;žWɤÃT—Q&¬—±ü¾›;Š«„Á heXi;AÂpï,17˜®ËÅáálµ(­¸6oÐøMS‡†‰áÞ廳ޤp8!P¸Ò32cƒ¥1óáRä@ì° Á(s)ì™rm…AÙ>SÏ Yn\Œë:ÁìãkG/ 6ZᾸt (Þ#q§`9%\EɲŒæ/Jß’¯xupÿâ†O0!Î, ’®y®v褞…u†OÁ$€%jR=J‡”–DûøBd`ÖÔJ¹¤Š&wT4б¨„ÁhÌÈb4w¹åšáÞÙí@Þ`510XH’[¦\Ÿ4M&xL‚°J'—û—ŽšÂ™V¦ØÀ&‘fŽæø˜NSq؆r”Î «'þ7h+À¶ß»NÜd!Zh FŒëæØFçÆ(Öws9Í,½é\ÒÖüSˆï›w$jÂ}Ó6â´Š}çr,NvÓÈœ™¡dªáðŠbivÚø‹ÐA¡”"ñÔs¢Q »·éHé‚™"'û2S ¾zmmªÍbvŠ÷•foµÊh/V&F±q•0˜&àå²Ì “à—>3è›ßnØJe¾SS¥PÞ·ïýª›¦ŽôÂtôœ>håÖ)JÐ/«]F7i.Ÿ˜u¨>`ô;äóh”ÎéK1úÓA[aP¶Gï-Áÿ@`ŒÌ°«s+ç!ó6æ»ãóhÓªÙLœ]5á¾8TyW¯ƒè®¢pjÝ™7Øá{“P‚“uÉ_Ù¡“zN¬F^‘Qþ&x°·j”¦°í¼Yk^ôivÈè8{:б¨„ÁZ²æ€gÂf9è]¦“sr¨ô ã˜Z3Õíùåò§ ׂ2y]V©%´Ü#*JƸS2å0F `z؆ÿß(Ó—bô§ƒ¶lû]wôv þCj€1âCY{ª>Sûõ‘ð& ^c~Ó.°óÈž{:ëqÃ<­Uñ’ùÌBùDó\ Ä¦›:©/Ú81öñƒÝ^žÈ%½¼´+aW ƒõçä4NY±ê–‚%ôÍsvØk¦eE©3Ó¹«ÿbÅó9Þ~_%–©~h0œ?+ÍSŒåψ—R}ž3Jçüœë³Ý Ú ƒ²}¶Þòܰ#týàdUß¾Ç}ÁÅFžŒ,1k/ö²ª?ØÎ#\%0<¿dUˆ`„y.\ë×â'¡!aÑáµ8¹zG1•0p«ã‚°xƒIob`^ÎÌç´ %:°®4ÄüD8ˆ¹OËŸp&𪠇>§iS¼¸ÉÉ(ÓÑÏ  Ú °Ý´æ:AK…¡& 0FÖ@ì,_ WÂ<"$O¯N 0øñ&S"¸èHŠcKQ;ï$FAqð‰®pÍi'}æ¦3—W^î­µFéœÆð)ƒ¶Â lŸ² …P·©ÆÈ î®Ýbd8 ctÈ9ÄœÌÓô‰ÎIÀú¬©‹…-”vì€NIâû©5úÉÇA$¶ùå{s}´¾Æ6g¢¢¬s²¼Ó†/^/>E×§:àÿ …Ò_6ȹ`éÂó×ë›2ëêéQŒÅ& f]ÃÀŠ¿ÙE&ýÇÃcyƒ5¦0ÀÑÍ,¤1E¦Ó—èêL_"`à''gê‘Âø9É=/Ò| å¶ãU£T fv>òeÓ:Æ^‚Ä:±Ð2‹‰Q:g‘ùÓdÚ °íôÛÓ´NØ­ÆÈîÍó±òýbL™ÉÙ.–É)ót½s ‹CQ|JFE$¸–aàH€?ÇÎg6C|ûÃ\èHdÌ‚àÂZʨÄ!ì$}t3ù£€4B5ØâË10UЂ%¥‰±5Nvä×4åŽb{¿2ŠÛ„Á:Bƒhù» ƒo(YOP×e4Ñå€Á$×9ÓŒSÀN•Àz $œ23ll…“`ú²ÓØ„±ÂP”ð%HXkó¼(¥™¡Ñ’–E.°9ëi˜/‰óœQ:gÎóùÒƒ¶Â lŸ¯ÿ„D7«ÆH\4Qš±™Éí¸:¯ÔÃ` ;•úÌ„Dñ‹o¤zÌHR{—réܬ†ÅèÜ+4‹ùCXaŸgÜ\òJ¡.ð!E>´/¾xúž(a‰Cÿ _MSú‚_åé(Æbœ>ˆã(¼éÜ`ݦœiØnÂà«(_¥iL½yžd'·êVOÖÔÁ+\(„‘¨–"¾Í%Öäãð‰gÀÎ=Kq%NTˆÜÅìÂàdfPsÁ‰S2F霾£?´`ûÃ1zóÿ¡5 0F;{N_ÆM‡Í’H®oÿsÕÃà5öòüäcÁs2,ž~·ŒH^ÈÓ€|"±}\IWb[~-›Ò™!a,þ™Bn\›0X@Wè7½Ëœü©ÒÇØÐ$ëÄ4!Lî\ ®¼»N½P*H(Té⢛B¨Ì9*E«¡ÜeùrDÛÒrmL1ô´ØÏO}YÃÌy-£tΜçó¥m…AÙ>_ÿ ‰nVŒÇ.áôPP¿D&Xœð’’œc­*ÆE å ¨òfÕ»d ‹É…Wd»ÐwÖ4£‹JŒ·?§Üž–XâÞYv¦–Û¿eN[ò[³] cèíŠÂ5d¼huVÂli9{êßÂÏæ`¥sú’ŽþtÐV€í}}~ôö þC•`Œì2Ы=}<*vq˜ð’ÒrŽ…ÁHÄ! Ësøv¬RØ+“=>`@ùˆ†‹öôjü<> 4ÈAbþÿì½Ï¯/ÇQÿ}.‹¯Ðùgøز¹‚,€«ì£,¼@øì"¤H‘ `Ý…e%; ȉð¯þÔ¹í¾3Ó5ÝŸž]ó©£ÑçôôÔôT¿{¦ú=555kâeeŽ+¤Á¸FãE Ä xï`uñ vÍ»o;ÜŒŸ¿žÉa)Mr±ôPq\Ç\S\Mh2{uÃL“«oVØÊɹÈ}¶ctŒªÝç9àZ®‘ÈZõBœXy8žéŸžE2Ã.’)¢[¦éÃ*7+6¸¥Žt@“ç°XÝòèé±Bpòõ 75Œ¾Œã²î‚ø¶>óûìŸ*¶cÙÊdQHƒ¹Lxq,.ñâðÞÁêàâÚq8 ^5=Å9Èj¼0ËŽpKå¬k<9Åñ;V#J˜±\PøÆ2±¦J˜½¬œœ±ƒ‡,Ô^Ö¼rp½S—Œ×ˆÎ~ãVñ)Ý*5á¹Ók8[Ò`^Üæ½•ð¹(ùnÔ“Û’tC=Œ/š(bóyƒWUX^‘ã9,ÃÇè—¼¡S¥w7>:Dà™ªÝ÷¶2ÇÒà¿eU2½ŒëÓh°­“‹‹Ä9ƒÚÜ›Ëk§Tb£&O¤ö“S.Ÿqã´ –p£æE½º£0±’ÁXáíËÊÉ9†åH5FGÁ¨ÚG:s¼/{! ìtöè\#‘èê ; ôFÄ(HÎ4¥…ÅgUh0:ðªuºP3ÛÍÝĉ„ïZ4¬n©žši¡äƒ@*—ÒAèw+œ'´Ìb‚YY™,ΦÁ #ž’ÞqÙœ7˜NÅ/_ <ÞoNc*±~“çsûÉ)—Ï ñÁM®~u‹0·ŸÒW GË0hYV­œœ“ʦÒè( ¶ |˜óÄ;Òðf=~¹ x9§[uö·Ê "S •÷qÂ×7Ê[rkÐ`f:<™q·j®wýÔËd‡HÜ>ò0wë5áÜÀ{F€"cº Æ»ÅjÆO…߬}+s\! Æ Ï]*Ñ/,äjåwL}ÓšÅ/ت!8Ck@‰Èå4¦w€CDçsÌð0h³ý䜤ÁšÃ-äUîêrÜ Gá¸:PXV­œœ“ʦÒè(Uû0§wd_Hàƒ»2,|‰p°T+®‘HtõBÌL;ñ#L+Ê^‹Ïª¤)¦ÍTÿ0M¥èLe:)3Ù±0S£žœî¢X Á§”wáâà=›fªVÏ8§ÇBm ÛË[™, i0—¼œoƒß”ôŽËƒ‹kûQ¨=b ÷×7÷nãd>-úW<'c¡ê “4˜¦À9Dû¯õìZ)×”´ÀÓ4„…½+¢›•“³ IsÂFGµÏ;ÕÍ +ìä€úÂT™à„CŒY×׈Âc›`Â7 ·Àt³:¬.Nƒq0²¤td6ÝP*¿cÚ)¯“£…8Û«†rtqªï¨Éö}ÏÑÊWHƒãW3…1õMk¿`sh/UÏ ¤2‹‡Ö¶¹Ø`¹Ý(#^ƒ˜©A1žÈõ€'&º0ûZ99Ý<تÑQ0ª¶¡“‡§í¸}éN~fÌt‰žaq³‰áClÀ]•UyÒŠ"±á,8@ùµfUÞ;y]Ì\)“ÚÒ‹Cþ²±ñlšGÕã§ÝöØÊdQHƒSr[^æ‚ív€&ã.œcòÂìäU6¹ÛY•r2sDîa H¥ 6M®"/7ÅpxÊ“2i¥•“3Õùxe££€Ú%çØñÆk›ñåî¥-sztÕ†éx!7š¼>±}z‡§õ„iËŽr! –‹4œo/.³4xYm×n·ÿBÆà‡xk€«ú«Î|ŒLLà /Á)s‰… ú\ßèš`ñÂü)6Xž›(-³É™ŒŽÏ6[Žj»7x½3Dhp$TëÈ[>l>Ì“+ì—«ƒ+¢œË|HÝÕ0Má•Åà7fŠ`:`úŠüJúÉ.Ë“D^'G(eÞ…aÒa÷Iá~*%ܸwˆ¢þ"Ϫʇ;½¬Çh‚ÞRÒìàeƈÅ7èWíTcãVæ¸BLH0ȳ86XÂt¹Ç—[\eô7×Éó¡äÌçá&HÇ»+Op¨¤ñÉ6¥’–1J˜#Üä©@xò´‡-Ã{%w×]¸)>}ÈFi™MVNN½Ö·£j[9[œ÷ãÜûRNm1 "Ìï¸å®jdVýì“÷E+îP˜ ÷Òû@ƘñcM8CàiS‘¨¤•}–­L…48P¬ÖÝ¿ƒ­œ}ŽÎ¤Vâ¡åRâ|æÄ£@—'ÉgܽäÌï¯]ßÐxðëòGå¨oäÐðp1¶½œQ’ÆiÎ̽0»gÖ-˜•“3â|È‚ÑQ@íÙYøãµM§œoƒóyGaFKƒrpŒéëdÌ ˜÷Á2),•è0{"3˜|• ésMŒ –ñ³GÉu³ú03^ßÈ!o¹‰Øìèƒ1ó±À,ŒVb&ÿXí*ã‚›+QZÿ£CO­Ìq…4˜«¢5`¹³«%líY±ª<×7ãÐà` ær'–œùœ®°Sþ¤À/‹Õ\_„œo:c$'…¥5pŽÌóOÕa¥1€Î¨Úƒ^t»ê4¸Û¡)WŒkDá±é&<‡L¬é"®’TfP.™U‰ ®/UÇ~˜´fP†¶1+I¥<‹ô¹ŠÚBz¼ûì‚®UôtÅ{AKöJ¬«:Þje²(¤ÁCžìÇßXЙðà†tŒRo5!êæä§Å:A†19 jîÍórËVîm¹ D“¢ƒ¦Þ•KoäæžEWÕÊÉ©÷ÂúV££€ÚœŸÖÁïV§ÁÝM¹b\#îš[eŠÄΧ[1øzêà,W(OS«é¦XF†n‰WÓܽ§]2×‹ÂøÐ nddtÄá<ësÕÊWHƒá„ƒ§6²:KƒÍ͘œu\J¨)˜U^NKÞM£PxÊ.´¬Û™{B~zW‚²ø“¹&ï@[Q?@¼©Ï©dåäÌéŒz££`Tm+çŒÓ`+#¥èÉ5’2ÛÉ2ÉÅÑÁLJEj(ºar©,¡Á¢9ÂÃôSþO zn"&&¼[ȰP(ŸÑ6Þ„Î;ƯÚÙ8:²TŠUu¦q+“E! Æß(¼OcºÌÒàµq^¶}^a‹—?QÁºÝàõ4<äðR$yÇŠÉ™,i°u³g2ÔWØol Ì(ÄÕ³ VNγ;hbG££€Ú³·‡&ðïSI§Á}ŽK•V\# •MòrœÌªã_e÷r,Í2=ÝU; áò$T¢2X­êò^Â̼ñýðŒUíã^J¶WF‡ð†ÉÊèX™ã i0´Œ4:ïl-¿`ÛÏ“EZ âBP¹²”ˆ\ŽÅÛ©˜,®c–ðúÛÕX´¢÷øœÀœÆöPÐ9öd;Nƒ'a1ZiÅD à5ªö Ý®: îvhÊ+¡ÁDÜ,Ìs|õ€—SrL¸|Vå.-K„¿+×\4Ì_ÈàïâÐAøêAôåöÚ±þmQ›·xÄã-oöí¨ÕJ‡–nòÌWÚOŠûˆƒ•É¢;Ë€åήrÁF@L`³rïÌ­ºÜ­çÔæv axiäV=9ÙJЗÈË™occ#zÁi°Ž­­VLÄUÔvoð“W/æ^M•Ð`a¹ tõ ¸O9BqwËBå^*­wÜøÊ¼b<²ëº±e+s\ –§ÿ¸1ã¢3á ¶êwÇËe%!èz2‡q6?9W§MÇ“[˜Wh|Ùg2«)‹.é´y‘ËÜÊÉY‚‰]££`Tm+ç‰Ó`+#¥èÉ5’×É2AtÔÃ9) ~YÜE* gUñ™ð¯)SÓwNg™žð¦ò†5G/I7”kj›zæh\Ö¨M!.âÄÞF-"££.)œA¶T8ËÊdQHƒãç3À?]fip¤ÿÂàÑC¸ÓÌßZòð‡0»Ä~Igl¬‘õØ$yD>wzaûˆô@rvÕ½Á³°b"¢öø$ÈøêÙ8 >º~vœ¥ÁáóWàœL Ì㥃¯KgéÃG³7–§™hÅ¡QŒhŠ~ð,ÔDŸ² éSLF‡w‘X#VûÔs ••9®Cwß“ÕYlhÆwn¼çBs%û ·äœ˜¯ØÅ4<ÞBòþ%…ë›ûg7êç3çR\u¡8@ÁЉ@mTíA/º]uÜíД+VHƒy²îŸëßÿâe:­.BƒQ˜—¬™Èämk=o0Žæ;ðò–5›n¨Í$™yõf³Ãmy ‘›F…‚Zee²(§Á0^îD$‚ñ:f+\qËS¥ñXœZ\D1]oç/òr\LæÀ¾±œîK³ñ©M,„Ç7ùÒÝÓr Á¥5ç•­œœçõÎÊ^FGµ­a+gBª§Óà £e®…DzI’Ë//ŒŒe÷  ã=²ððñùŸ~Ù2³ q/óä–Õçûõý?é`Pt°Ú·îgk—>†>»‘ v´2Ç•Ó`<Ÿx5Ó…ì‚:.¿`7‘µñôŽà«`pžÞUb’3϶pÞ^ƒf­œœµ¶jtŒªmåäqle¤=¹F;ÞD|¹˜X(ð8Ry?Ž}ËgUZ#úf[š˜Ùy I8)=õ×ÕŽo¿ Z‚GŽtÁ6S`u{¶8bBïymå&ÅÊdQHƒ¹@~øHh°¼§Iy–oq†ìw Øo¼; Èð¢˜/Îy®îeó™[99Ų»ÆŒŽjën¥î€6¥Ó`SÃ5­,×ȘëNÖ@zã "s+«íŸÏBHƒ´&™êy€‹»fZÝgϘhæi»<¯<ïae®ñUë´I99+ÔpUõ–jœqŒoßc"èõR-¯ÚŽ•9®ËÉé•÷¼(È ã, >öŒÉݱ:  ¬VT¬æq¼&ë>þ„ÐâÓÕ½ ’VNÎ1,Gª1: FÕ¶ræ8 ¶2RŠž\#“¤w\É|æÖëI,Ž&8ÏX2Ö”xƒ í#foÍR&`é‰fs“ˆ¼u"nU<«,ä‹P:ØÏ&:{ç!)]ã÷`±ÁáÑÀw¨q’0:x¼*/{ÖÒ`˜ö+þaEaÂ\°ËjÛUkøi9ã«’`-¾1—ª:~&E Ðq c÷&ãØÄMû˜#iyÜHzˆª²3™*¸V6: ¨›LWꢚu|€á.§ÁXx–à†}çÇ,ò>55J %4X0D’ ) Ñå7o˜ŒÄ§jÇ­*SpŒfŠT<Þ¹Ž÷\Ï7VüÀÜÅÜ»»íŒÀZ™ã i°Ü´†+åêežÑËõ«p`6•_°=ŸŠ9Ýî¯ÁüôLâ“‹t/b´¸§ƒ÷Êz<¨B˜×r'9ð}ËÏžµØ1„ –«¤låä,é‹]££`Tm+ç‰Ó`+#¥èÉ5·zA¦ÑTF³*©ƒËgU²zFÌ!8VNg™q>ü0¼Þ™dV2‘’+¸I¯oðÈ1¥²È·ns}´X/.n4—¿,â3Ñ+“E! †ÓÊó.%¸Yá·•¹`M ÖyJrBr BqÃÝèÓ; ¬Núl%d‚­q+))äÎf±c,g`çpçé9ÞËÊÉ9ÖüH5FGµ<4 ‹ôÅið"0îÛ×HÊl•r˜N „yäÍ&‘“x̼Vê©€e.ÃÍ…F¦ø}1,<:}ä±)ó©, W¸£]1L§ ý­Ìqå4&SK™_})¿o51¦c%¡²œXy¢¤0[ìLÌ–6éNFéôvÃk¯?«ÈS³T ±låälìf绣jw~2DõœG(ì¸Fê›nŠ^ì¼üÐ›Ê Êµ³*žè"òÇVIÊ/Ó¢%=’>2ošpb+£Û”º×„Kä$»ª·2YTÑ`ž;ðp_~u,[í æ|ó"Ö;“ž«-g#ç9w¸\Ñf¨µn¾Î8•“óŒ®ÚÅè( 6ç§!œm©ê4ØÖxMjË52à®ÊªÄŠ[˜_8ðR Ó˜˜¬åUk UÓÓâ“Î$Pí•-}l?úf-0ˆ!ì™çÎOnåõùÍÝr +s\9 –Ààô¦•+K'õ÷­-€ï²ï6''Wú²‘ÿVNÎ]Æt³ƒ£jo6¬rÜ`»s(¼w¼)ýŽÆÏÞk,֔Ϫ§¦ìÓ{U!~ïp—ÐGA”‡Î÷Ÿ£5ûae²(¤Á’‚±¸ÿþ#—Õ)„i–î²{¡CÛœœò•çܶbåälëeï{Ôvoðzç–Óàõ°Ý¬e®‘”¸*eBv™[Ç‹²K! ÆyÂôÄ3JÞ.!Ò˜«UáÍà:û@—ÐG‡²xL ‘-a(=6øì“fjÇB,¤—ØàÈ{¥†¨þX3.^°SzÙ¨[üääºæ…¸Aç?ûä}ÐT¶¬%`-]îp_££`TíO€I•œOÂb«²œK^™LÓßv'x® ÁܺÒþd"#[ئÚ^B鯰ýtì>øô½‡nËV&‹B<þX†ø‡ÇÔ7­w;@튭qrbýîïéÍÜeŒ-á(í ǬœœQáCŒŽj»7x½ÒiðzØnÖ2׈Âcã&b!pÕbðqôñ&HºD™q¡Ð¹$¼—.àp†úÑO8ÐÁ®ÜKè#'­dŽâÛ‚|¦–ÔvDHB 6;™[deŽ›¥Á,‰\ŸUI˜†/4%½ãráÛ‚óŽû®qrJútBŽõ=%3—wåî¹ñB½µrr.ÔÝN›1: FÕîô$©å4x‰½ ®‘1}¬o’þNÜ`ÇòY^ÍÄÁBûzÂ4{?×øúH_%w1Þ8$Å'ù€®ÿ[™,tŒ7R®£Üï˜ú¦5Çö¯qr’æ‘ü‡\Ú$7ïñÑÏRg¼•“s©þöÙŽÑQ@íƒù”º:=œw5ç)Ã52ஹU™Xñ2ñ…\|¶ñ7'O}9 æ:%‹ Ó sJÌ!|^§úÜëú(ÈÓS2GñË8òQ¹>‡c ••9N§Ás†»Þ‰“E^;}¾š’Þq¹ü‚ ghuƒ“·3FlAL¬œœ v¹Ã¦ŒŽ‚Qµ;<&Ur< ‹­J®…Ǧ›Ä³Ç$Ëã¿tIeåêYõéñ{¶tmu`º@'[­L: 3ÛªšÃ{ƒåûqñ”ó“3Bá…Y¬˜ˆAGPÛÊy>ÐÜĪÓ`ä+YNƒÿñ·BðÛé[r÷…Óê€ú¦«å4˜8RyµD¢u}«#°8Væ8Á§%ÿöä¯ÎŠË/ØÅñߦA®âGÛ—?âÓ;°å{%o/÷gåä\®Ç=¶dtŒªÝã0¥“Óà)TŒÕq¤ÄU/¸ ,Ê.å³jx…çú†¸  %â™SH=d PW×&V& KPD.0˜úYlsôæµæµMnäa–Ü)ÀWüÈï„Ê[rDvaʸ™×©XÂÊÉYÜ!“‚FGµÝ¼Þ ç4x=l7k™kDá±›Êi0Ó[zÍ·êx¥rZ¹ ~ ËDÀʧÓ`X.LXYfiðQgL¾qÌ]6)ÉuC™|D­C\—:Ûñ3Ó !ñüIæsžp-Õ¸•“s©þöÙŽÑQ0ªvŸçÀX+§ÁcLÌÕp4r]e÷ |}#`dBaG NƒÍNv¶2YÌÒ`!ºòt^'½ã­rÝÙÄYÍé` ŠüiüÎî5+ ‰ã‹½ãų-èVNN½Ö·Ô>ê½mg”ÓàF¡Q®…Ç6n*§Á¸%*˜éöKr6YRH?ÄÐØSßÝP°2ÇÍÒ`!À’ÞPÊüJAÿ„¬¸ü‚UìyÉÏ# &Q9f†€ã—¦ž=½“¾‡´u×7‹´, Z99{úvÝŒŽ‚QµÛÇk›œoƒóªGáiäºÊîå³*“1u²ð¼R <»\µïÞ¸#°2YÌÒà˜Ñ…kj°p§9ö§5\°CWˆ>Ûó“Ãxá½¼àÀ‡•¹ãv²g/ •“sÁ.wØ”ÑQ@íïÈ:—}Ur¼/þ‹kDá±›Êi0Ô—K•‡‰ž0m‘aõFj°2ÇÍÒ`ƒ‰€LAù$•)é—Ë/ØZx»’;#¼XJ1îÙa²,ËÑÐÊɹ˜}¶ctŒªÝç90ÖÊiðs5\#\WÙ½|Veb‹")ž(86w"YWØÊd1Kƒ…ÜÞûðQJt7~±&Ý*åÃ{ƒWµ3¼÷öÏä-¹e/+'ç²½î­5££€Úî ^ï\r¼¶›µÌ5¢ðØÆMå4˜ÓGŠ|ñêú†ÕÍð9 `eŽ+¤ÁBk °—[K ³ÁìR~Á=gÖ³3$d»É÷éÉÓ–½‘·rr=+ Õ6: FÕ.”ÝÅœï>í p4r]e÷ÂY•)Ì3J\X|¦YÒ -;´å-+“E9 –¸ˆô—ç,cpZsloðŠvæÉ-¶‹Ðkn7øe!NxÁëÅÊɹ`—;lÊè( ¶{ƒ×;œ¯‡íf-s(<¶qS! &“5]æ¹-4˜«ž#b³sÀVæ¸B,™"ˆ‚""NoÌ¥¤w\.¼`ž0+Ù<Ì¿ñË¿ ¼ä æ²Ìê"©Øj+'§Ñ£Pm££`TíÂAÙ]ÌiðîCЮ×H#×Uv/œU¹WeÖ ïnCƒ?ýè'¬ú lûàz åX™, i0W D—T„Pb)H͘ýÆšc{ƒW²3òª71ß¾\Ö‚Y99Ë/7‹’FGµ}2]ï|s¼¶›µÌ5¢ðØÆM…4˜ÎÊ7”™;8₉Œ6ƒÑd+s\- †“B ¢+þa=B¸ü‚5:ÜëÙ ‹4˜8aüð Bdåä\°Ë6etŒªÝá 0©’ÓàIXlUr4r]e÷òY•ÛU&’.˜¬žLU1·§-0][»X™, i0N`n* Rå—²$³¥¿“.X»#X¢ùzvæµ×Ç–K´š•±rrÎvÄ´€ÑQ@m÷¯wâ9 ^ÛÍZæQxlã¦r<|!îÉífø°2ÇÒ`Xn †‡ WH)<É~ceùkõœ–ÁjC¯R¦‘–šürW+'ç—±dtŒªmå rle¤=¹F¹®²{ù¬JZ§È„ù2)^,Egßä,Ž€•É¢œCná½²H9ÒÝ\áðÞ` a!|ë7(X±3VNÎůʮ4: ¨½ø}YW㲯2Nƒ÷Å‘£s(<¶qS9 ÆUEŠ!ݲP`u‘Þy#Ž@!Væ¸B,—ù»…–úò ¶ÕÞÄ0,a!E994N‰Ê{ÓpR+'ç¤ò‡©4: FÕ¶rÚ8 ¶2RŠž\#\WÙ½|V%šŽé‰ŒC„³8 V†Ì7­€•É¢Klð=ëî÷{˜6ÞÑiðÆ€¯q8®‘F®«ì^8«Êôôá‡wÒÁ@ƒ¯oÖ謷éä°2YÒ`²BùtÒ;Þzlo0wÙÇ™ ¾²rræ.®cÔÔvoðzg Óàõ°Ý¬e®…Ç6n*¤Á|n‰);®Ù·¿Ø ?#ˆ§_çpÒ`.(bƒáº¼ —1õMk /ØÎ!RÔãSBHä•¡à++'§‚ü6£j[9aœ[)EO®‘F®«ì^>«þàßÿÛ©¯2L¾im¬L…4˜paƒߔôŽËÇöN!n·IQ>¨ìsÕÊÉÙ'zKietPÛ½ÁKãvœ11WÃ5¢ðØÆMå4Øn®ðÁ°2ÇÒ`ò¤ †JLb„Ó2¦¾iÍá/X¾ïöÁmîÔµrrš¶Ja££`Tíª¡ÙQØiðŽà/uh®‘F®«ì~øYu©QðvvGÀÊdQHƒ%ëàì÷2RLùÀÞ`Þ†c!QÓä –%æ*ßýôÓ°rrê½°¾Õè( ¶{ƒ×;÷œ¯‡íf-s(<¶q“ÓàÍÆÑÔˆ€•9®Ëg” ]}õÀlHÉxJ~Å7~}ÃW{O›mv·rrnƒÆ^G1: FÕÞk”kë4¸±å¹F¹®²ûgÕ‡ÒUjAÀÊdQHƒ%62‰ÚG½·íá¬ó黇Qp¬#àבõÜRÿµiðQgLh0/Ä}÷öó¸ì4xËS×ú±ìÒ`ëÈ÷¬¿Oß=Žëf¿Ž¬ŒTz®Mƒ{èã:.˜%mù¿>ýÌip ˆ—uìÒà£ÞÛêãµÍVŸ¾·ÁÙrlü::öø.Û»µiðQgÌÿçKü#<Øcƒ#^(AÀ. .éËœ‡€Oßçáæ{9)~¥hxYG`m¬ý[?x㯠–ïM›è‘QfÛr%ŽjõÞ¶|ìÖ“ôé{=l½åËAÀ¯£Ëëöž®Mƒ/aÆüÑþB"„=opû y9-Ø¥Á—3FÛ÷Ô§ïí1÷#¿ŽŽ7¦ëõhm¼žæ=´L,ÄOÿòw^ÿzÈAáÙÓ»´šÕÁ(›í—-££àÞàUO3Ÿ¾W…׿ü:º^¤›kÓàc{ƒÃ”¯o¾öý÷X(°ºÈ ¬ÝˆQ¶6,·otŒª½ñàž}8Ÿ¾Ï†Îwt"~E(¼0‹ÀÚ4xV»¸‚I !(°Jeÿ=r&ÓÃÔ>ö½í¾ç†OßûâïG?~c·éÅÚ4øÀ3¦dH‹¼WX1•Û \ËQŒ°–.w¸¯ÑQ0ªv‡'À¤J>}OÂ╎@~UÁuáÂkÓàà ÃÇýKšé#V?ÿâçýwÙ™LcdtPûÀ÷¶»Ÿ>}ï>®Àðë胸YÖ¦ÁÇž1åËqo~çŠÌêf×r £¬¥ËîktŒªÝá 0©’Oß“°x¥#P…€_GUp]¸ðÚ4øØðBò¿÷£»_úý°P°ÂùÉôpZÔ¶rž÷0ʵ:øô]‹˜Ë;cü:câ59Ö¦ÁÇŸ1ŸÞñý¸gOncpê~ê°~\D££`TíE†lƒF|úÞd?ÄáðëèðC¼`צÁ ªÚcSOnåûq|BŽÂÿ~òo=*9Òə̒*ŒŽjÿÞv‡Óáþ>}ù8øutœ±\¿'kÓàcϘßúÊÕÕÃG|EŽ…¸V×±Ž`”€-Ðóžš0: FÕîiä5]|úÖÐñmŽ@~•áäRµiðQþùçÿÅkqï~ü…ô‘«Tößeg2=Œ‘ÑQ@ícßÛî{nøô½/þ~ôc à×Ñ1Æq›^¬MƒýQ`Õ¦msÞà(viðÀï¶ >}w;4®˜!ü:24X»«º6 Þ½ƒ«*ï}ýëWâþ§?ý5¾§¼êá–jÜ([ªû´ctܼêùãÓ÷ªðzã‚€_G2Ћtsm|`o0øÌòÚëCùú&~Xy‘¡Y¯£l=@viÙè(U{—!>ã >}ŸÚÙ»€ö‡Þùr<Þ~çÇ/¿üUúuö¹á;^kÓàc# Éÿøž Õ§`åÏ™L#etPûØ÷¶ûžNƒ7ßßÔ|90Nƒ7»šLhm|ì“\Á,Eüô/‡åýÐÄÉ`”€™À¶\I££`TíòqÙWÒiðfø Ôo¼ñ/Çó…z@€ñÝì\ò™F`mlœYå ÆŠ>~å%""H˜Fö`Ñp&3;²Ô>ö½íC¯Âi°β›êeñôÖ£¬Mƒ!÷Ò7¿ÝóXGÝŒ°¨ÿ1 FGÁ¨ÚVÎçf›”C½Ô~ G gÖ¦Á=÷½]·ïÞ~NH°¤Mã—Øàö67hÁ™Ì ÏÂè( öïmgGmmçfk#Ûw¨#^p.µiðágL:(9ÓÞþ"¤6q.%`&°-WÒè(U»|\ö•tn¶6þ ,ßú¨I0+GôäZk#ïí;}"°6 î³×‹iõäöÙÓ/t[áüÎd;2: ¨må*žÿúwò¿zÅËqÄS ð›ß¹òÏgu¸×è—Q>‰ÚǾ·]c¬ËÛt\ŽÕy’¸‚qüÂ#Ô°_jŒ^çà{9Ž@D`m|Ôóÿþß¼Gjˆ_ÿÿ¥üµï¿'ŸÏˆÀö\pƒßÃè£j÷0â%:DnV"ì2g À”$¤÷³OÞ§€7Xˆ1ÈŸÑšïâ8ÖX›[ÇGן‡k1(‚×.pËËú^»ou&³û €ÑQ@í£ÞÛöpV8 Þ`$B †Ç0‰ í‡pÞX›{Æ$œ,Òàßý!4ØD°Þ®F}ŒŽ‚Qµk³Ýoµ¼'Nàø»ÁqýŽ€#Ð!kÓ໼ J?úÏ_ðei×åX°ñõšr&³¶å-Ô6q¯W>]I: Þf8ð`à&(B~ý”Þv?Š#Ð!kÓàË1/PbR›ø3JÀL`[®¤ÑQ0ªvù¸ì+é4xü9áÀ²Ä'zÛÚâ8]!°6 «*ÃëÆ_üôµU±TãÎd–B²¥££€Ú—soÛ2¾çíë4ø<Üj÷œ…{ºàZô\Þ8kÓàcϘäI#»œØUbƒMœF ˜ lË•4: FÕ.—}%o†¿„CxºàÍ÷9}"°6 î³×Zýï'ÿFŠ`Ü¿ð^ÞŒ{Æ·äžÜÊ+r-o³»3™mpÖbtPûØ÷¶ú¨­½ÕiðÚÇöÉ7Øèe{áGÀhD`m|È“tÁààþ½¾aù_þMYÜÜx6^ÔîFç_£j[9µœo6RÌMÐ`¾£±Ùý@Ž€#Ð!kÓà»Ü®¯ÂñBV”B\¨qÜŽíå´`”O¢ö!ïm;9ñœo9dKÛòp~,GÀèµiðå̘Á9ì±Ážâ½ªd—÷ŠèôrÜ2Š\S¤}`RãwÙ2Ä4è"ÐrÂø¾Ž¬Mƒ/d¾Ç'•MôSfBÏc+itPûrîm·?·`û%âwì§}ËЬ´¯Q+ºÞìy¬Mƒo:žÞñº/Í={zwÞl¿—›Ží1Ñè(U{ŒŸ5Nƒ[ÆÅip z÷usdqÔzÓymÜ[—ÕöûÿüŸ+b!X(2láÏMG£dtPûø÷¶ûNƒ[°wÜ‚žÅ}ZQ‹PXçµið±gÌo}åê—~ÿ¼ÇBU§Š›Ž†Éè(U»‡/ÑÁip J9§Á9dŽZïæè¨#»e¿Ö¦Á[öeãcñá œÀï~|ÿe ¬Æ¯il¬LÕáÜtTÁµ’°ÑQ@ícßÛ®4Ü…Í: .jRÌið$,®4jE<"»¶6 >ðŒ)4øËà§wÐ`Y(Ýtôp©£j÷0â%:8 .A)'ã48‡ÌQëÝud·ì×Ú4x˾l|,>ßÎxüÊKá+rOï(°Ê§å6VãŒÃ¹é8´Åw1: ¨}à{ÛÅG¹¶A§Áµˆ¥òNƒS4.¡lÔŠ^ÂÐêãÚ4øØ3æþý¿¡¾¼^”»¾aÕÄлéèa˜ŒŽ‚Qµ{ñœ— ”“qœCæ¨õnŽŽ:²[ökm¼e_¶?¾_^Žûîíç,L¸‚AÉMÇö§ÊøˆFGµ}o;©-kœ· í4¸=‹ûµ¢¡>°ÎkÓàcϘ¯ýФÁ!(âÉ-VMœ*n:z&££`TíF¼D§Á%(ådœç9j½›££Žì–ýZ›oÙ—í%邉ˆø_þM‚"XÝ^‡3Žè¦ã ÐßÅè( ö±ïmèªWÁ5v<äð«F­èáÇÅVצÁÇž1_~ù«¿þ‡K8ÄÛ_<ã&lbôÝtô0LFGÁ¨Ú=Œx‰NƒKPÊÉ8 Î!sÔz7GGÙ-ûµ6 Þ²/‹HàA†´>}ocÎ;œ›Žóp[v/££€ÚǾ·]v”k[s\‹X*ï48EãÊF­è% ¡>®Mƒcvx2¸éèaPŒŽ‚Qµ{ñœ— ”“qœCæ¨õnŽŽ:²[ökm¼e_üX…¸é(jU1££€Ú~o»Þ‰á4¸[§Á-èYÜרµõu^›ûŒÙáÉ㦣‡A1: FÕîaÄKtp\‚RNÆip™£Ö»9:êÈnÙ¯µið–}ñc"ণ¨UÅŒŽjû½íz'†Óàl· W±ï“Û„«/l[wŨ]o½µi°Ï˜•²…¸›Ž-Pž;†ÑQ0ªöÜhô²ÝipËHôHƒç(" çÿãŸÿè§ù;䟟í;bÏžÞñå¦ta÷O?úIÜ÷³OÞbÏž¥2ì%•Ql\(ׄ ùô‹Ÿ¾öñ»?üà¿.I˜_Þ8Š• »9£×Ô"°6 ®ÕÇå7@ÀMÇ ÏÂè( ¶ßÛÎîÙNƒÏ†Ž{ ÁCø§ôâ¿>ýL§ˆpH²Í‡äó,WXջ̷¾rõ¿–׿ T¾ñÆ¿Äß~çÇÔ¤”Ù‹Ê(3.TiBS’0?j>n0­©j¼JبMÁñòî¬Mƒ}ÆÜ}ˆÇ ¸éc²}ÑQ0ªööã{ÞŸ‡›ìµ# ×+j@ñëÊ*ŽY…|"IþÚ÷ß#ó<Ë·ÿ¿Ÿ°JAù iº¾ùîíç,BD¥Ò`Z¸xøH„å3Oì›k¹J„éÔ‡ÞÉäÎá~å•c$^+#à¦ce€‹š7: ¨í÷¶E|–Óà³`»ßiGüø•—ào?ÿü¿ ˆpÀ²PPhðç_üœ­xŒE{ éê$4øo¼%›~ðïÿÍ*eømJƒãŽlE& Ó8Œ4nM UšÀ{i ÷2Qôš_$ÒÖåªÆ«„9Q+:€ÈW÷E`mì3æ¾ã;yt7“°l\itŒª½ñàž}8§ÁgCÇŽ;Ò`<®8E£'Vü±Á ›E`rd+áµÒ埽ÿ«ð@±<Â?úÏ_pÜñ´)õ+×jÁæ“©¸¯é&¿øœÇG5UW s7Gg/œÀÚ4ølÅ|ÇõpÓ±¶å-Ôfª*ï¦KV!à4¸ ®ðŽ4x ‰¬Šƒwr“TBM¡”„)üúþ­"Ì&pÃòqࢊ|Fž(⫇árMÄat~z÷ŒŸÜ~ðé{JËl*o¼VبÕáò­#°6 ösã-9œ›Ž”Ö–1: FÕ^{4—jßip ’=Òà<ùdr$0²Ä9Pxû‹™Þ#› QÁ§—ÔÞýXÛáá‡á*M†~£Ãoüòo²ÀÞY½k¯ÂÄÍ‘‚¼o*D`m\¨†‹m‰€›Ž-ÑÎËè( 6óZ®S^߈€Óà{£Áô¥äb!d7µ;Fƒøø- …Ù½RáqSJÞ2ø%$CwJÇ£Tu³Pب˜x¡Ö¦Á%F .J7= ·ÑQ0ªv#^¢ƒÓà”r2=Ðàý»?™Í{õ‡ìñŠ™ä= y} 3Pÿx-T^ÇcGE\˜$b´ÌBhÄX˜˜áÐÈóð r бp¬´Û£À PÕÍ*a7G¨}õ Ö¦Ág¨ä»¬€›Žµ.ißè( ¶ßÛ–Œïy2NƒÏÃMöÚ—¯KÒà—_þê«ð»dÑ™-™ˆ. €…«z÷‰…o½=½ƒÜÊ+uŠ<ü– r¶„.À0òoÞ¾C›1¶Êt1LWKZN嫺Y%lÔŠ¦àxywÖ¦Á>cî>ÄcÜtŒ1Ù¾Æè(U{ûñ=ïˆNƒÏÃMöÚ—ãÕÇ@ÊBï)ˆWqØBJ¡ …9ͤƒÂfaª$I 9„ói(¤qa¹Dç²±ÅðZã•: u Î^䢰å¸{U7«„9„›£ˆ³ÎF`m|¶b¾ãz¸éXÛò–Žjû½mù(×J: ®E,•ß—s]°üùŸýñk¯?–2ü¦šË&Ôâ']HeÚ£A™ *d•ö¥ñ@\•Æ%1ÂÄ-  …(äЩžJò‡ª–9JU7«„iܨMÁ÷òîp­÷÷½W_Y¯qo¹ÝOƒ¦«U-³oy7«„Ý ÆÅWGÀ°‚€QŽÚÌ€V@6§§Óà–!ë£? Lj+`™ÌQ¦u²ÍýѦ’-Ýbà1õ¬¦[seÚ'4BOpÁ¾¤Å AÉÃ6kª4©6jEsà{½#à8—ƒ€QnTm+ç•Óà–‘êKˆ/!À ýì„32Ubô¸Z¸o\âæçèÄ g– ‹I¤òI6~ƒæ§á{5$'Æœ´S¥I•°›#e˜|“#à8=#`Ô€£ö¬ó§gØ;×ÍipËíNƒ¹4 ²ÄBð>Â!àöêA$ºiפ’­¸U)³|öÉû¬¦2ƒ2A Ò8G¡ÌQXl\v¤5¨8ßhf AÂùÆi„­(LƒèLÜ«d™( «"Lv8 ¢ NCWƒ½Ê5©6jE'õJGÀp. £ܨÚVN-§Á-#µ; –·Øè4˜OWÀWa€T:Å«sPGÉfFá/~5,H¦«’Ï!Þ„Jã:Y%°´€°T,jË/’BÈs9+DØ©¸oªjZÚ\¨I•0Gqs”BíeGÀp !`Ô€£vœˆ ¡mEU§Á-#Õ †F’6 ,DwÌ'qäâ¡Å› 5%Ûƒ,â:Vº/ü“$l"#ÙØÆËVa§d?ƒX²ëKtD®qÑ“ x8ÞiB)ø”FNXèwü€²d$ΩA#UšT Ó¸Q+šÃÖëGÀ¸Œp£j[9¯œ·ŒÔî4å!$éŵËÜÈÄ;›Ÿ!öW`\,HãðUŽ¢çs@Z[èj†–‡`æëù˜2O* •4K… SP8³ÈWiR%ìæH&ßä8Ž@Ï5à¨íÞàõÎ+§Á-Øö@ƒññ†@Üë›?¼ƒÑųý Î^5É0-ÄÆiŸ£°ªü±•Pq5ãfE˜‹Z¾¤Ì¯þ1e!ÚŒûš2vÑÿª4©6jEu¸|«#à8—€€QnTm+g”Óà–‘êã§…R A¥ÀªÒ£®ü V“-_…¯ò%e _Ö¾XšlG'«ƒ]táöl°ï`ëxµªq]ØÍÑ^¯qGÀF 8j+ó¯ ä{VÒipËèô@ƒ  ;™¤)“o+=zýëW!·0邟ÜR`Uf„“@ â¤ñÉì¾D&³ÉtARoœ­³š¶9i QÖ[FmtŽ¿Ó;X+lԊ΂àŽ€#à£ܨÚVN'§Á-#Õ &\P"Bt„šüA‚l ¯•ˆ\]dB°ñÕZ– ‡Iþùö;?FFB‚…0#Æ!ôÆÙŠŒ® ÑΈ¡ªHÞ Ÿò çF jÍòòË_}õ~WBšùÍy«„åˆnŽrÈ{½#à8#`Ô€£¶{ƒ×;µœ·`»; –d|‡"øiO^S=6~Hü0±¸<úŸÈXhl0—ÝŸí û9~©Q¦Ñr‰&ª¿õÛ¿‡ÚfÕæÐÄ$£vø•¯~\ßL²wt¨–óĨåý×pKFÀ¨7ª¶•3ÍipËHíNƒ!ŠâVÅe* .Ó\pŠ" k|ú^,§…à>}r+Ç„izv_vǹ ᄆ´Oïr>X$Ë5‰Âi†4NÚTÛ´ŒÎ,¤{íõÇR&ª™^§»Gù*aÙËÍQDÏ Ž€#àØBÀ¨Gmf+[PÒÖipË`íNƒ…O’B!zJ‰‹ÈõH\ÇÄ9ª—̽°Ögƽ6=¹Åg8íÉÏ ÅÅ+›kœúàƒ}øMÐÝs>X$£&è€&0íœ&©0á äLãûËŠ°¨ fh¤,éŽR%lÔŠ þë8ŽÀ%#`Ô€UÛÊ™æ4¸e¤v§ÁÂ'yy-ÆÁÆÏLLö+|2ã-¡e¸NŠ…pßç}¥PKLkZÐ œ3¤Î4.G,Ôäa‰s8ÅÁ2`ø“û• »9šÐ+GÀ裵ë¦ÈþG¢' ·ŒF4˜X_ Ä}ã¡À_­ê”Bƒ l AI†F¬oy2Þ¨@®q‰L×7´»ÈoÜùy!Mz†Ÿ•žo™þžh–S…®v•°Q+: “×:Ž€#pI5àFÕ¶rf9 n©Ýi0Œ²'q¶\)¡ FäNv6ÇT&œ &C“pߥ86oÞÂ!ÑU"x9ü6${þDZäpt ‡­¬JŠç"Óÿi‡Xˆ—¾ùm"„Eí*a7GÓˆ{­#à8Ý#`Ô€£¶{ƒ×;¹œ·`»; !¸'ÞKJ”¡/¬Bk«:E®†yN# „-/Õ8GüðÓøÙ3’›¡¿(O;*7†£ŠzÄ)M!§­ì»C›´†õ@>MÛ§P%Œ¼Q+švÙËŽ€#à\&F ¸Qµ­œcNƒ[Fjw,ìdýúÞ«¯Tå«eªÂo'q 5ˆ¢¸a‰³ÕS.L¶kœ<„sHËð[˜°(Ï—ãb;8rqAã1Ži ³o9"Nc0 Ñå76›ª„ÙÑÍQŠž—GÀ0„€QŽÚî ^ï4sÜ‚íî4IDÙÌ ‘PÖà)=åg¨êTŽ©Òˆ´ MlÈ!\ù—k\Œ-³*ÊÏ^ìÑ®(B¸ªÒ ¤:|ZŤJبU°òMŽ€#à\F ¸Qµ­œTNƒ[FjwŒò’Ó —)nÒ¯}ÿ=Vkÿb`ÀxG’¤EOlpÃ^ߌeôš\ã4Eƒ±q)”d¢4X¥µèC;ä‚ãèO„°ž0­JØÍ‘>ܾÕpn0jÀQ{ÖAÔ-æý+æ4¸eŒz Á¥úO&a8Uâ°U¾=Gûº@á Žk\šŠ-ë½@,˜5"¿E’e°{Úå*a£VtÐe_uGà0jÀªmåsÜ2RVh°|9ð½Óg‘åWR–I÷sÄ’]H,ï¦QÐù$Á½d6#¥ƒ(+KË$ac™m™ð²ÞIy ¬ê£V%_%ìæHGÞ·:Ž€#Ð-F 8jçæèn¡6¤˜Óà–Áê„GvJ!–Ó~ñÁ8"x+(b"fù•¡¿©Ø¸¾D|}Ã{j![Úó´fc1©O"Cû¼˜FÆqç„¥edˆp˜m9ˆ"ŸQ[ZÎ5+õUòUÂF­¨—ouGà0jÀªmåŒrÜ2R=Ð`8À}o “®VÉW s£V4ÅÇËŽ€#à\&F ¸Qµ­œcNƒ[FªL¾¼¸j!¢H¡ô¶¿,,Vî1Ù„¼d÷¥M]> CÈ_ýƒß%0X¡Á"ŒIhð§ýáœ&B°Qƒ¨`¨8Âx¿•>VÉW sP7G ò¾Épž0jÀQ;7?öŒ¶Ýœ·ŒT4ýã ¹Nñ‰a'¯¤±PøÆ«s’R A¬êò:Dù>|DEß½¾ÑMhóíw~ &æAÑDrµÑ8®oñ~+Âlª’¯6jEu¸|«#à8—€€QnTm+g”Óà–‘ê„—w²øç‰¯â7ž¥Í8™ÉèK /‡  Ë³•dþ$öxV˜yóŽÆQIÁ¾!#ÄÔ_Ì~Fãzz7Ù[”á·D¾\ØÍÑÔàx#à80jÀQ[ŸO @ß±ŠNƒ[Ç ¦³ðL²4à ÎÎ!]Hk*Ê“™ŠÕý%üxRmy…¬A¨­ËW µ¢“z¥#à8…€QnTm+§–Óà–‘2Gƒ%¾W¼Á$°ší>$öéá ß{õ•/³ OIöÉûêÁ­Á;—U‰ÔoºW›S¶EÔG)îcTÉÍQ„Â Ž€#àØBÀ¨GmÝ™ckzÓÖipˈآÁÄ@M‰@à‚¢LUR%L"À›n!Iï)68ææ=µ±0Á½ˆÑNºÀ© ë5“4mEmÒJxByóÙÕÊÕF“*aÑܨÕa÷­Ž€#à\F ¸Qµ­œQNƒ[FÊ ’o*)À'qÛN" ñ½EÞt“rŽ –/ÍÅ_ˆëR4x]MÔNó§¥ú—«Í^UÂr7G)Ú^vGÀF 8jljÛÚVTuÜ2Ri0 Ǥ˒¦ n¬ @D} ÜËÃHyÙm°gVZžÜ4,4÷2œœìj95ÒKÔŽòUÂF­hì¬GÀ¸XŒp£j[9Íœ·Œ”-LOÉKö­¯\‘Ô—…ÂlÊ\Á‘1ûù¶ftáÆš’ÂdPDT3Ÿ j<|¤·V¥v•°›#yßê8Ž@·5à¨íÞàõN*§Á-Øš£Á„C}%¯/Võ?¢ t‘pb]>n…ÐÎrì( “Þ`¶FµùL>~ß9î8(T©]%lÔŠðñUGÀp.£ܨÚVN0§Á-#µ/ þßOþ¥JÞbã¦Rbb)äÞ›lù’RyQŽJ„RÈ3­ !Ír q†ŠAƒ¢FÜe¼uPƒ¤çws4@ÌWGÀ°‚€QŽÚ峘•±èGO§Á-c±/ æèÄ Þ•ý½þõ«(ÌЇUýïéí²}*„c)Oïॴɋrf8v¦åò‹]‹»È4®è>Ø”kܨôÎWGÀ¸@Œp£j[9Áœ·ŒÔî4˜l D8@†¡áss|yhª8l)Ì&s2§ö¹ C(ÅÕ…¦òUdø8 Vl«Zžl'HŒðz»9š ¯tG ŒpÔVfÞþaï\C§Á-´/ & ¡üJÄ/ôOû´Ä³gÊ^ß¼ù+!Š”•îK¤.¼’Æeø£ÿü…B›¡ÖlåãËÒ Vs᪖sæ‰WmܨÍaèõŽ€#à\F ¸Qµ­œWNƒ[Fj_,Nݨ?/ŽÁëöôî?þùÇå[o¿òÊmôÇNö‹ð –à:~ø‡0{MŠI%×T–C°—¯*t¡Ð`޾hùüqîÊ­j9§aŽ¯Ú¸Q+šÃÐëGÀ¸Œp£j[9¯œ·ŒÔî4Xh-œ–TfD/ÌþÁc¡Ê¸jC¨Ã)ÖWÙ%ðɇXB„ÃI˜œiŠ<:Ð>,”ìjµ-OT£Á5jW5îæh.¯tG ŒpÔfÒì^£: n¸}iðk¯?†£²[~¤¹‚ßýø v§û°âðzÝÔÇÒx̱6%u_‡¤D)s ºVU-O4Œðzµ¢“z¥#à8…€QnTm+§–Óà–‘Ú—CöXø#¡q,96û8 ¦²šs«Æ½ B|üî4žoa.ÇÅ -œ„Qž/8¿PÖʤڹÆÝ…±ïä8ŽÀþ5à¨=˜»÷‡ò@8 nÌ}i°h.Q¸!HÜ«µ=Z€ÃKŸÜ?ü½W_ I۞܆eêîø›TÝ1¯ï>_S¬vlʨú{Áp‹EÀ¨7ª¶•ÓÌipËHõ@ƒ!Dá¾ôÍo“)‚PøpL×PØ5…ã^†µâm¦M ¹Ïgð/è*~õ€ Z…×÷Fo¿ócÑãBþ *G²Ïª„ÓÝW;6îæ(BáGÀpl!`Ô€£¶{ƒ×;Óœ·`»; Žñ±ä4{ãáJOæÒ”åz Ð, âãF* v,&‘ÃaÞ¡“rŽK;d¨H8³BƒSIÊ9áT«ÅÕŽµ¢Q/8Ž€#p±5àFÕ¶rš9 n©Ýi°8rñ¦BA¡ÁBhù­ê”â †Ök§ýðûtñð‘rOŠÿð÷!½0¢ÉXŠN@ï`Î,;¦¿UÂ鎋«ws¡ð‚#à8¶0jÀQ{<™ÚB¾gm·ŒÎî4åýÿö[_ >Ò7oß!A’?TuJ¡Á¼‚‡wW.@ òF^®qÈ'É+dkÐd*Îar_èî²Âë©mÔŠNÂ€#à\F ¸Qµ­œZNƒ[Fª å“/)“9aýó“U‚"&ås•hy2”XɾQµ§;Ó_­Óà–1éóMä.ø”£Œ«ZžÞ‘í̤5£3?@ÿ´½VØÏ$õ„|Ð9¤P;¥Î§J8×H®žxÒx/Éß`³ã]sÄ¥Dh4Ò¼ Çïäîã½ÆpG`¾çµ·Ÿ£ÏSÕâ^Nƒ[F­LT- \.¤,›û>2´„ñUŠ0ÞËûF2I!–2²úø•—Pàkß…«“bg+íLnånBtƵN¸õ¤;wlE )ARØ/&Ì55y ¯tGÀرßQ™òCU»¼ƒûJ: nÁ¿ õâ-9ƒy…_(e®GŒ¯ÑÁÖÂÇ”‹ßb˵™Öó;ñ¬r 2›ýÓŸþZº5-Ã69t c¦ÀjŽ6W §G),ƒ™+B|õÕtÆCn®ÌÒ` ~É)—v+‹ô¾8ŽÀîøi¡1<¼˜Q>‰Úî ^ïätÜ‚íî4X(bš(øƒOßËõǯ°MÞ¤CsÄX]Ðü[¿ý{4ˆ_”œ”Æ%5D$ã^¤]¨Nw,,Gó&A퓚_z1ns„>ÜDû¿ôz°ÄM^pÝ€Š/g¯¹4ìÒàK©-ûë4¸íÝi°0[²“ŒöÓ~‚?6×#ø4a±¯¸=¦škD©'ý/>U\»hEh‡œ°h¿û,߃Îò*áÜ•z|¿( `tÆ£ŽpL7ØQ¬(S*Cgvð„³°Uþ(ãLÞÝ æ 8ŽOm¸TÁap-ûê"€}¶ØkÔŽî‹úw®³Óà–Ú£|øöÄéÃ8cÅ›ëQà“±ðÙå°Ëõ iÍrÂçÕÇK5ríN þæw®X(èÙÕª„sGÌÕst–_yåê;û—ZQú×%Bø°œ ÑÅ=Û” 8ŽÀø·ÈV‘p+:£§Qµ­ ì&¢e¤z Áè/ÙÒ$ÿ­ž€æ†P¾œÃS tNÍÞ b-¿â=Fa :mfk&ŽW>Cmî Ä\Ò—IsD˜õø„áÃüR¦¦¤5—qµð9nm„ µ?iÀû×µ•Y¯ý;×ÐMDËuBƒI€FÎ1âê.–<Ñå±~xÇ-/Ђû‡@š²òFˆúà<ú²ÃåÿÎP[ÒµÁŸÓ…ca2>ŽnEyðÊ)!Îa4ïî5Ž€#°1>Çm xÏ‡Ó x·šU»[<й‰RµÚ †L†Çú§  „þŽ»ðÙ'ï²÷â±ÄJ 1]£5Z€y/ëRPë§w'N éŽÃ¯ú'Áñ¬æÄÏP›n’Ô,D³P r6SDNîD$08'àõŽ€#°>Çmuÿ2Ê'Q»ÎÁÕÿHô¤¡›ˆ–ÑèÃT ñåa‘Ú1eå%x]êí¤ 7¦2×ý×^L³¼ïFÐbé,_͵–Ö6RŸ"r)°Ðø${—½Pa”‘ØÞbC~ÜG>CíP}}C¬K`Ú§üiΦÁig½ì8û"àsܾøwut»4¸+¦Œ›ˆ–Ý—C¡¾PĘ$Mr‹S. ‡gü…L²o®ûiÌVA±lvÉÉ—×KX2df+åð–_>o°dÈѶÒǸ:8îjÓ)Þ”v ýÒG¸±Óà¶¾êXDÀç8‹£¶’Îvi0ñJ˜x³n"ZÎi0‘«<¾Gy¼—¸RÅ;J>9¾^ âíL…|æºO¬Å}ìÁ“[üÀ„ à Ur åÚÉÕz²U¾ë1fï²U¨~ $– œðjƒ· r¬Ÿ½ÿ–ÜðÒœ¼K8ÐߨôÂWËAÀç¸ËëÙž5àFÕžŽNÜD´ ÄŽ4X¼Dü‚zõ@|¼„Å•tJܪ9Ih*-K63‰Às»à®àHƒ{ÏüŽƒÒ)œ´Â„)ðÕ¼œ&g¨Mkô”øg zª 7G9ä½ÞèŸãú—]´2jÀQ{ìÝÚÀCÔMD˰îHƒÏpðzŠCU¡|t ò)Ÿ“XãŒÑ h%Öó£y}q_#¬'ø=Cm4ç‡;ˆÓk†|czTºjÔŠ¦]ð²#pQøwQíwÖ¨7ª¶>ýluÑ2;ÒàœÚD·æ6Åz"($ˆBá·´›Šò±…e ¨¡hRu¬óÔ† K”òd Dª€›£ /;ý#às\ÿc´™†F 8j/5Enµ¡¹‰h¬Þh0|•ÔÁZžÞG!9Á(Väyv)‘WšjÙ$<œbt`ÑSs *µ«„ZÑð}_GÀ4>Ç™¾e•7jÀª½ìحך›ˆl{ ÁTˆœô‚ÑÔcƒ¿ò’¼éF˜Võî‹| l @^om°Z+5Âr[ÓUúHÞQ†@ƒc—S±X®R»JØÍQÙ Ž€ |Ž31LÛ(iÔ€£¶{ƒ×;CÜD´`»# æËqòU 8jxwŒ/P<¹•, ¹AŽ¡XUø¤È“3äf°P º|õ¼Çk}$£€ S_¬Ëü‰oÞ¾ÃÑ Ü•7ûøÍˆW0’…jW sD£V4‡•×;‡GÀç¸Ãqyp£j—˾’n"Zðß‘¿ôÍoCö>y±K¾C!Ÿ¥ÈõH$ÄO„*|RäæX’D—#*ò¹ãNÖ‡V\=€`KÊ Ê¹»]QC’ª!#9Ír«Jí0Íê}ts49š^ét‹€ÏqÝÍöŠ5ਭLyÛÃx°#º‰hÐi0¯táðäÒw»äW¶¹ Ç‹_"¦ Oy<Ì|øL¦b]>wÜÉzÒ>Þ€Gš·Ò((‰E ÜÅ]2CË'Û”Ê*µ«„iߨUàòMŽÀ±ð9îØã[Õ;£ܨÚUC³£°›ˆðw¤Á“j3špÅÉMR)ƒ#°$ÖÓ”±‹ÈÓ&1 a%a¯rÐÜ&(¨lŠ…œ$_ý€*‡øäÓ"zË S_¥v•°›#vßätˆ€ÏqÊ^*5à¨=;KîéŽë&¢e{£Á\)Š[•ž" nXX%…Ù+ ((©z Ê….ê9u[œÝMðó‹ß2¬ËW©]%lÔŠêpùVGàÀøwàÁ­íšQnTíÚÑÙKÞMD ò=Ð`âÒp_V•!ßbSdÒMU§;.[¨ qÕÛ¯VûéOo*»9RÀñMŽ@‡ø×á ì¥’QŽÚ³³Þ^à¸n"Z±LÄq³’PWh§{ôôŽG`Ÿ~v*Ê7÷WÚø\;ço__mrVe!9Ù(èÙØŒZÑóñ÷=ãøg|—Tߨ7ªö’#·f[n"ZÐí„“,‚¸–@›‰ &'ƒä‘@@ÒJÀ÷`€z÷.i\o¤eëÚjKº âCè&¦&2J/ܵŒ¦ïël€ÏqÛcÞípÔvoðz'•›ˆlw§ÁÂâH!½àJ‰iý’T°Êo¼úXÊBùB$@毼ñL T¯­vL˜öòË_e4Ñüù?Ï©nÔŠæºãõŽÀáð9îðC\ÞA£ܨÚå㲯¤›ˆüw§ÁÂâ÷…²B{úV²Ò#hð?ü} {üÅL¼²:þ­m|ÜÂR5ë©Í¼9„„‹8 ^jÔ¼G |Žëa:ÑÁ(ŸDm÷¯w ¹‰hÁvwŒò$1ƒ¹ñ!6â(+=ÂüÚëE€ˆ]¸¶q帛ÖS;x¼¯oˆÐªi(ŒZÑFü}wGÀ.>ÇÙ»Å57jÀª½øð­Ô ›ˆ`{ Á¤#(‚´f,!îõÒ#„Yän‰¼"̦ªÆõ¦Z¶®ª¶Ä]p¢DH7ǪòçæHÇ79"às\‡ƒ²—JF 8j»7x½sÆMD ¶=Ðàþ:‹K…¹¬ ¯¬ûˆ‹gÏÊO´ly_µZÑe‡À[s !àsœ¡ÁZ[U£ܨÚkæRí»‰hA²LZ]¾ Gδ_|ôÖîŒ÷…Ó,áä#[È´¶÷*}ñÓ×t-ÖSÛÍ‘Ž¼ouzCÀç¸ÞFdG}ŒpÔ.ôY툭ÝC»‰h»h0ŸŠ ¨5fKûô£çQä ¸É®A¥žB,OJR⇯op ÈpNrÕzŽ»Ã;Ò\«vÄa£VtÕÑñÆžð9®çÑÙX7£ܨÚîÙ‡sq6tìØ †ÔŠ2Ü-òºÈ LåÛïüxÜ583o‘V‰78xÕ?ÚùÇ7ÞR±±騺ß2ÿ÷“ÿ 5å¸áãw§O¼Q`U?@•ÚU˜¸9Ò‘÷­Ž@oø×Ûˆì¨QŽÚî ^ï´qÑ‚­Nƒ‰UÀ- =c!yWáiå0a¯—¾ùm T¾ñÆ¿Œ»ö[¿ý{l"9Ø›·ïP`ÉæÈ}r‹ÄÆ?ûä}M~|°æúÂ9EñHK>ùð•Ù¶ëÕ®ÀäÙ3£V4 —opŽŽ€ÏqGáŠþ5àFÕ®˜]EÝD´À¯Ó`¶BØð¾ât­åÃå4®bžÜâ8åX¤&"‚ã¾ûñDÊüÆ|i¿.ì—Ñ“ÝÿÙûoÑnX>*ñ­¯„ÆY8 ŒqR~¥JÞÈ# lœB\¨ÉÑàóÔ.ÄDúèæh¥±öf•ð9n%`-6kÔ€£v¡[Ìâ ì®³›ˆ–!Ði0ŒÂ—ȇñÓâŒÕ]Äå4X>‘~O.SùÍñ[:¯¦X# ¾âØæ½–ö7츕uj8csjœ­v„"rºµ¢¹îx½#px|Ž;ü—wШ7ªvù¸ì+é&¢ÃÊx³,|Þâá£H†)à_Å?LHªÂ„Ëi0ã ÎR)´tŠ}aƒƒY nX5)qãA wÇ#ù¨íæ¨p˜\ÌèŸã:ˆÔ0jÀQ{ÖEÓ¼FupÑ2p: Ž,>Ì‹f7ùâ[¤ÄQ`\(§Á›]ÄH|ðétE †…ûòAê¶3’Â-«¶Q+Z²‹9ÇCÀç¸ãéÙ=2jÀª}ö0m¼£›ˆÀ i°°\Ü¿_y)r` cökÊip‹þ³ûf ±ÿúô3â:fwYC€— n8ÕO~uVõ£¬§¶›#yßzT8óåX¯çÅÿw‘2¶.6¸F+Í;keYÍzíÞ/†iwÎPµ7ów¡žõ]œ·Œà, –d¼\ü’_ßà&^"’Þq¡œ‹›”ÜbðUœ¥³±°¿Â~ÑYZfU2Eî¾ Ÿ9=àÂP¦Àj$热­¶Q+:@ÉWZfmÝØˆÖ”ÛºZE»Áë†X„™óºd/£ØU»dDzqÜ2 úÔ0xEŽÀZa¿HÌNåSŽÈ*ò´)L;GË{Š×ªIŠ`¤@†a ¬–7²”$ÁÀ7ÎbXL_¼ÚnŽ–VoǺ­›5hŠ@¹­³…ØÙÚº‘9ºÙb‹Úqœí£ Ô"à4¸±T^ŸØ aKÙ/®Züü*“‚l*Ÿ¾ñêcŽBÖ/2¤…4µ_/x\ù,®WR«}÷ösY8J»¿MYhpL_L5¨}µZÑ1V^ãT! ÛºYƒ¦”Ûº*…í »‘YoìŒbkTíõÆqÙ–·à©O x}á¨âûG%UYøÖ›N†Ë§H/i„úR€»®ôGlð.4˜îàE'½¿,Èx\ÞÇeÕvsT޼K ÝÖ),wvS¹­;žJ_ÜÈ(à4n2Š-j·{·¡;ðîNƒ[·dj Mß΀@˜°2At85¯ìÃG-p½/Üê|Ý×7ª¨þ²jµ¢g#ï;:‚@‰­S š²©C[·ï »‘Y£ØU{½q\¶e§Á-xN U‹gX¾Û˳{¡ÄHäf‡N¦†A˜ñ`µºª}9.ÔWRsS<«Æ@`°ÍÛ,qµ¤àæ¨%—9…¶.gДúNl]?CæFf½±0Š-j»7x½³Âip ¶…Sƒ^ÜÂÐ`fy¯­LŽ 2QÈÇ”)„¯6ïñW«F¡<ÃÇ ÏžÞv˨-ì‹99 mBws›.‚caÜÈäέ ëp£jo8°M‡rÜ_áÔ Ùn &®LT°c%eDÕÔÓ&àöÌy>Ïë&zJ0óý‡•÷xEÍkÕ(”gøÂ@\ßÐÇûsÌMUnŽÎ;‘|/ëÚº×Uê«lQÿüÏþ •w#SÔbF±Em÷Ÿ1Ü…»8 .jR¬pjdq]=DîÄ-œ›ʧÉfµ† Sà3Í“ªžW‰¶¼ž&kÞñcʵjÊ“û‚…p ~%ö72H*Xµ¢J|“#P‚@¡­Ë4¥¾ÜÖ•èÙ§ŒÓàNÆÅ¨7ªv'ƒ>«†ÓàYˆò©!0áÓ^°_Ì>þaÅÌ”Q>5Ð ¤3Ë^´—ºmıLƒdZˆìò1åZ5Êå‘d‰½cDftsáòÂE!PnëÆ;¹©ÜÖ™œO½ùð¤‰çM< ¤Àj|x—뎙2íõF±Eí¥¦õv ׂÓà–1­šˆ †héì7NåSƒx2Ið+é¾öý÷Zz”î+ {ñ6Œé&a/Æ<ئ\«F¹<ÃGïˆÙã;×8Ò ü [2H*ý2jE•ù&G *[íXI¡ÜÖ•èÙ• OÐ0),ø=xÞ$e¼ëJº‘ÑñiÙj[£j·ŒÔ–û: nA»pj 7®€ñ¢ÌUSC¼OŒ…–N¥û±ƒûá#|ÎâvN·nV®U£Pžá“Èg:Hú2QÌþ¹9š…ȉ@¡­SlZnS•­3Š-}ÀBåÝÈu†˜QlQ{ñÉý ôŽº‹Óà–‘-œ„@Ê÷ô77/PßÏÔÀÕ‡Ÿ™_œ84ZàjÙWÔ@‚4JÔ(Qûµ×C€ñØ \¨›Q+ZØ;srÚ:Ŧå6õcër}o¯'hn¶ãF¦¨3ÄŒbkTí3h—]œ·À^85`a¿¼k†Ë1]róB9 &´•|_„œÉ2›-þ÷ôN>ßFð@9]\\ Ž~Êûä¶(D¹@mH5 d&.‚%d‡SÿÜ©ðøÆÃ"Phë›–Ût 4˜{íÙXˆx긑‰P,^0Š-jï9ù.> 5è4¸e@J¦B‚ ‡€SÈMãú’©ZHf ZÀ«)‰¿f‰\Ui°y¹èÙûÔ—sYŪQ"L/jÐY2-KCújÛ¸…*µcþºÐÍ«ž)bŒ§×8%¶nlÇJjJlÝEáo”ª™#£ØUÛÄ)’Nƒ[Fªpj@òC± rñW™#J¦È!ÌMžés«Hh+ѰĖ ö…ÀãÄÆeJã‹. À!~öþ[ÉõVa¼(á¾XƒP˜K_\¥6ÝÁEÿÒ7¿ÍۋҸ±Ý­7ÐÞrÏÚ:Ŧå6•غž‘Y\772‹C4Š-j»78ââ§Á-–L òñ8aÂâÉŒ¿¹yú’©c˜[ª?ÏÝ8Pšâ,ÝzF™Ö>üðþ kDÒ’—F¨„ÌŸÑÚy»ÐŽÈ¾ÀEÒA(­•«MŒØ8ÂÔ°ªhÔŠ*Xù&G [§4eS‰­+Ñð02ndÖJ£ØU{½q\¶e§Á-xN U‰+¸ÿ=}¹ŒrãÔ€3äûJþHS©³¸D¶¨ÓæxGYŠ€ Á!àöEû/!$ÔôãwxÿšÉI—«-ÙÕÀÆ¡ÁŠ•ÄžnŽ–RoÃ…¶N±i¹MNƒgƒ™ ®ÅµÝ¼ài0hÊiðªÕ©ÒÈ–…'ïRÈÍ Ô—L ÈÀHy9.>Äçr WuAÆqÍ! ÀR?6‡Øò’¤wÜ2Ô7$v8}Üy¶UjõA»0F!ÎDШÕGÙ·:³Ú:Ŧå6•غYõŽ$àFf½Ñ4Š­Qµ×Çe[vÜ‚gùÔ@<0µÐTYHJéÊÍ ¥4øGwäcç{ñ!>NÿúCmgáŸöïÞ~ž.’]¡¶©y"“Y8.ÊÐ_Êú_•Ú4KL5»eƒ]ÐÍ‘Ž¼o=*å¶N1k“›œÎ72@\5Š-joézZpM9 n¦Â©§íâŸÄ™Xñä¤ •µSƒÄ-,~¥ÌÎôÖÛ·Jmñ¥C†A%ºÖ'Õ3jE'û╎@9…¶N±i¹Må¶N¿6}©ì»ïª™õð7Š­Qµ×Çe[vÜ‚gáÔ Óð7òüŸ­aèñ"Sq$#tÂ}N³–^]ؾ@²Ì‘rùÉ-VÜ)àø¦#PhërM©/§Á\›·Ïí*ýR­>cà ÙäÇh”ðí*a72g Gá.F±EíÅ}\…ˆ]‚˜Óà–Q.œ„ô†Ïõ¾ócÂ! ÄT.Bƒñ*Ó¡,$§YK§ûF#O!–2¬ÆCªQ./£ƒ»žðc¥;F­¨Ò#ßä” Ph뺛ÛTNƒ¹Tq)p¿/Ÿ§gUѼJXigrX^(Àn OàÃù¿*ašq#“Dzu‹QlªÝ:Z[íï4¸é©A>¦Lxpœ$FB .œ {XûÎJÕÈ[º&ûÃŒªdiÀ÷‚µ'°¹½Í3Z¨U£Jžýè'Ü­(zº9RÀñMF@·uŠ)›ÝThëÀ6¸U 'ãÝŠÓ·ã±u àUÂJ;ãMb4H6.›$[{ÎêV KƒndƘ/Uc[ÔvoðRçÀ¸§ÁcLÊkô©S÷Þb‹Ýý*DùÔÀƒ?¦ƒ7¿sÅBÕrýK$ãÕ %{-. ú!¢X,ääáÌ2·âC7rN’z£VTé‘orJÐmbÊf7Ú:q«ÊG-yˆCjÅÖU —t ƒ¥ /èñ^í³g![»IU%Lƒndh/¸j[£j/8p«6å4¸ÞÙ©A‰þÕg‡Â©A”ç'ÞZ ³¬¯¥¿GÝW²¥ñËŒJ7Ý)àø¦#0këtƒ¦l-´uX6Ø&kxIAåU…*á3FMü᫚|\þú&¾1ÙT•0-¸‘™„q‘J£Ø¢¶Ï시“8 ž„¥°RŸØŠ¹&°VÞkæ!Ý`iŸГø·,«…Ê»XD€W¹É5ÇBÀ¡nmŒZÑØS/8ç! Û:Å”Ín*¤Á¨ÍËqñ;>òGéK•°ÒÎä&¬·Ì<ˆ_zR&VV ³—™Ýâ£ØU{ñá[©A§Á-ÀêSæÇa|˜Œ·á(Œe‚(Ÿh¯ˆ°8‰‹héÑîKŒ04Äž²Ø'¬€àæHÇ7ÝÖ)¦lvS¹­ƒs²ð æNyEލ®8Gâ"ptPгµW £‰™Úá(—7Š-jëþ™r\rŒ€Óà1&å5úÔðo¼Eô'0Ž ŸQŽ‹¬*DùÔ ޝ*³ð@ŸÕrýg%yq #³T,y|ovÇ}ªÔæ1ë7^}Œ‘aa€Îæô7jEsÝñzG ÝÖ)¦lvS¹­ƒvB}I¶Ã+¶Ž+W¹T«„ Aˆb¢Ca¶ö*aáF&â¼xÁ(¶FÕ^|øVjÐip °µSŽÇÙIA §fh¹+¥9=J­¶³âÇF´ |[¥ˆµ¯'_¨6èŠIÒ$ÏRî¥ovs´Þ¨yË=#Pkë bU¶N27’£L.Õ\rH1Œ…µ°KãÑÒRP c•°hâF¦vDÊåb‹ÚLUåÝtÉ*œWÁ5.œÖ‹ –¹v±eáÙ\xB·ÜnR,<Þìó”ªÈÄÅX¢6¹Ñ»޼áòð½gÏ(Ð_¥F­èrg„·t¡Úºrö% i°Ø:IÁ*±L¹KµJ¸vD¥q±ìK£Aåd;UÂÒ‚™I$©4Š­Qµ² qÜráÔ i¸0•ƒ%NãBáÔ€ò|fåÕižRnéÑ`_fÞ4‘¹†o…˜ø+QÌÁŠ»†_ô9€tÝ™8\ÉÅ(´ucS6[Sh븨¹6Cv²«¼ÌeËk¹nV çÉÕKã1BLÏÖ^%,Gt#“C¾½Þ(¶¨Í‰ÔÞ}oa§Á“°V–L <˜“¯ÈÁWcl°” ¢pj@O¨)AòumU3XöëÄðÒÄ·Îc!¼~®ÞGµ¢—0 ÞÇU(±uŠAS6•Û:.ÏðúÃóìÜõ+]®VÚ™Ü$/fk¯æpnd&1_¤Ò(¶FÕ^dÈ6hÄip È…Sƒø«r—O ýñÙ®÷Ÿ®wˆA˼Ç2¨œ]EÏ êÓ»3Ž!“Gqs4 ‹WB[§ÐÝܦ*[‡OŒ |X‡½JXoj°•–…fCË)螺*aäFf€ö‚«F±Emý[¢ lÊipË N 8Yx_˜Çg¼ésóõåSá¬ññ\K_ûÒ,KZITžÓ¦•k—AŸé|õ”D©Ðf¼4ìł“oÏ}¹:*¹‘A²X…Qlª½Ø°­ÜÓà€ §†,Ü,þææ…BLZ0²"ð fY– ã¬öœ®áÄ&t6.•§î´üFÔ7§¯G¡ ýáÃOn7‘Ä9DŽÝ|~ %T9‚Æå€ü@ ]us”¢áåËA ÐÖ)6-·©‚?¹å%±ŸfžU W$V‚{m,¿^]æf1^ë£Àª~472:>-[b‹Ú³7}-°\ø¾Nƒ[N€Â©Â!͉΅Âó%7/Òà!6+ñr×7K9lÿ¼¾‘tÄhγ?¡”-p±/Yx* Q‰*Þo\|ü“À¼Htc=3¤šzzÇô\4Onåõó(3.µ¢ãŽx#P…€në¸.¸0¹ßTlZnS9 Ñeñ* —9«Jª„ïÛyr‹MÀZûôNiœŒÁbƒU¼¾Ñ5 –_Œ?Vúd±•–ÙäFFǧe«QlªÝ2R[îë4¸m}jHÍ>Áa–¿ü¿ß¿©À \25p{ÈÂûw|ŒCÊ!­ÙÕ%ímygQ€Eäi“óD2ÿ”·°ˆ$45e°€&sJîÖÇ;P§‡ v§3â5séW˜˜®o~ã—S*ÓÝe7G@|õBÐm]¸k>‘=\<6ªâÃ%¶%U&N[Gå$þU´û…Æ£|ìeœ½92œ¥Y«KSr/Õâ =©s¬t#¡X¼`[ÔÎMy‹Ct : nt}jH™-óì! œ–lÅþ3e-œJJ¹pj@yh0jH/È…‹_äzZ£g°öOCòΚ%Ò ®3ö¥k!¤áéy SÅý¢¼Å¹Òt’ïøÖ€Wlð)Ñ) q¡†Î*zµ¢J|“#P‚€në¸.ÄÏ)4’_ ðáIû–Z¼B[Ç¥J³Ÿ}ò¾hKÕñu-[«„±04…{Y"©° «ôˆ^Ä#¦(Á™#ÿÙûáÞœ#¦±,„<ÆcšBd—úçFF…§i£QlªÝ4Tîì4¸l}jˆ¦Ò‹,Tbÿ©$²wœD¢pj@yúã#•^„ͽ=]Ø_qqH$ÞRm:a¹w@zÊ ¥ÿ ¤ì')È<©êô½âV.F$®Ž nŽÆ˜xÍ% 0kë0bø5¤W+žÒڞҔĿ͒ÏÚ–ËåD§hÀ´›ÊîLgâÒ‰n"¼:i¨°²/›8ŠþÈÒ¨Õ{í[YJl˜/,7æbÜ‚Ý;-Ѳ å¶.ì‡hmá–º­«žíþ@+cƒÅžÒUŒR* éÖqÙÌ“¥jŒbkTí¥Fmívœ· \85ÈDƒ™&âÔ I$Z¦ôç…bXf·Uw6IõÃã<^v®n¡m¬ØÚ>…¸_f1·DAUEe7G 8¾éÀÚ:^_%ÆŸ'R±rá÷á#%4¢œ‹5;bÊñ²„½Jx²¥2=4úpë­§›ÆY‘ÖŒËndƘ,Uc[Ô–Ó~)¼§Á)µå©AÞÆbvº+¡h`¦ŒÈ‡L¸|jˆà¡5%‹BmïÇSMh6ŸE‚ñÎh¤}—Ú>ÞËKöŒ9L->ßœ}diÔŠ¶·páè¶Ž+Žˆ¬ô-³`Ù®oäÓ™JDvoÖÖñ~kˆ…8år‰¿¼*GU£½ï+0wú-?‡÷×'•(LjÇàW™e72ºÅ F±5ªöâ÷RƒNƒ[€Õ§†ÈlIí+t ‹ÄHð+øp”Œ…Ù©!ªM;¸`Úx›‰Íã@©§"ŠW 5”$BXVÏk§q¯Ú>Š<î ñ½°J9÷†æyj8^“Ñûèæ(£×ÝÖ±5\G²SCn™äÀì8;5ˆÚBùW m“Ž0Æå;£§¸‚Ñ\v„]ïBƒkû(ò<3µ%‰•“Ý—N‘T >|AOïB²ÐüŸ›£<6¾åÈè¶l4eDGD·-2á@‰£1 mܱ¦2“Wñrá3ò‡Kã©·áƒO§SÃÉYjS»»‘É k{½QlQ›©½ûÞÂ$Nƒ'a)¬Ô§†±Á/¯)œÐ§„Z0…¨ã…¦Ñ²PD²b L@x{ ‘YV¬¶"Ot àèj# ½—¨fm굢ˇ·v”غ{4xàuÊ ©˜¾B['··˜8.jî[y••«57åÂâÎ>ä/]Íù°Ø8žd|„=(š amjw72¹am¯7Š­QµÛÇk›œ·à\25(ö_ÙT85 iŠ;º‘‰P,^0Š­Qµ¾•tÜláÔ>Bqz§˜ÙãÏ)Í*KœÆ…Ù©AÔŽ©{pG`Z¡sø(Zz”îËí'›iE*eÞሩÌåÚ>ÖÊ3£…§«OnÁôpÝ3ÉÒñ8'¦}ts”¢áåËA ÐÖáSÅÛ9^Æ&.ÖÌÚ:q/CJCØ’äˆÈgЍfø0›Pk }E[ÓÆ(þ7t}189à Øo; ÞëÂ1jÀQ[9÷ó0ÇuÜ2”…S,XŒÿ`5α0;5Dµ«R÷Ľ ’~,Ì ¬Xm«äåì÷‘Õ’Žé=Ù£Vt²/^é”#Phë„ÀPŒ—hÙÆ…Y[œ‡Rà—E¶cý«„ÙSŒ¶ÜùIÅ]0”˜…«ãÆcMš x–3C°yý–¡ÁAxêãw±e ndR4–-ÅÖ¨ÚËŽÝz­9 nÁ¶pjË?K}Ó bvjHÕŽ³CI¡tÇÙ2-C)q°P`uv—•$7¿,(3{”rLð/I@ $X“ß\ûnŽrÈxý±(´uðCX%Ü2§KjÜå*[Afæšå“%Â~xŸ‹øÞÕ|zN|ÎùÏgÐ,#>-ÂNã±Æ…ðVÅéåe:Æ_=–IkÜȤh,[6Š-jï8ù.;¶æ4¸eP §ÄxJÈ“w±ÿ5ð CìÅâ ï5ãÊ`Áa²}!Uc¶U˜Ì¶–vÖ¨M»àeGà Jlwú˜5$…Ôšéår[GDAԜ˖;Vå¯\Xn±i0hy6op<´ÜDÇÕqMh™ˆb~Î…OÄÝÈD(/ÅÖ¨Ú‹ßJ : n¶dj` ÀÎ35°ÈtÀ^éêäQ45‚ò龄ï“R&9‚Zú÷•ç}Á•qòfð–tÜ´e¡B•1qs´å¸û±úA ÐÖ‰e#+·¢ÄÄßI+'•E¶îž„“ᔀ—BéÏÿU ç›Y`K­&nd=Ó„QlQ»Ê]“é½WO#à4x—²Ú©7¦åù ˜ýÁêäQ25ðüQâZ™z(°Ò¶,S¥Aáq²Ð”úXÊ@Z@ª\30a>Å×·™®Q`âV46jE•ù&G [‡8Æsͦˤ•«¥Á±iö>’_[¨.A` -!…XÈÈj­&nd&a\¤Ò(¶FÕ^dÈ6hÄip È%Sƒbÿ•M%4X¢X!~0U)‡WÃbªv¨/­ÅìdòÊóì½<ÇûÖªq&DëÑMg8dÂR&57Gã1òšK@ ÐÖqÿðèù#$Ym´u‚0—*Q¸„À/-+ÈW +íLn’×èxJEÎ"ÜH?)&•µš¸‘QÀlÜd[ÔvopãÐ+»; VÀ™ÝT85(S@nS õD )c“!ríL•ÄkòÀ‘WEp/ -¤@ã[^Œg«Q…IȰtú‚Fx÷œ¹ûá#¥F­èì™ìŽ€Ž@¡­“!òpc.…œ¡£¾ÐÖa…¸NÓ\¾ñ}¬y•ðx÷Ùqzã‚ ¿hÅ’³ºghâFfÿ³ŒbkTí³‡iã·^85(S@nSáÔ€ò°¸HƒSU=$…E+ ;ò î±ö…-,"v¶U˜@zñ! õ¥Àk,ÊŸ›#ßt`ÊmaEġ2ùÅqª¿1Whëà™˜ LQLÜïë9Ò«„Ï5ÉŸC DŠ å}º34q#sƈîb[ÔVœ3…}w±NƒsȔԗO 9º›«/œPÚ™ôÞÍu\.¼×^Û´à4¸ç©Sÿ³÷ߊ¿iòäR>5¤úcgmr*?_&ñBò‡÷#YÛ­X¨#Y–ÅÄÍÑn£îÞB['Ï «ñ²H^ø0ôxÒÐQYeë¸Ã劖ûÜÙüáU«¢[¥‰™õÆÂ(¶¨½ð̾Ä[vÜ2húÔÀ©+ù™&—ܼP;5´tAßÇNÌPDž"X¥.¿ÁVt˜Í`Œ „7³PXPm£VtƒqñCÝÖES&`žIIM=åZ\Їkù”0\‘³Ä Høé]0q/þ±ã¬‘yqm,4Açç³j»‘yÕòÿbkTíåÇo·àªO |&#>œ,Ĺc\¨ò´tAß—)LR±I66nêò+m…ÍF*Ë ˜úÐS&ßàŒº¾™U›PCi£ÄMÂÍÑ$,^yxt[-˜Ä*k$FB .·uŸ}ò>Ž”%ÐËü_¡0zbO¸YN—`@æŒLþÈÃ-´z°_ݶÄÝÜÈD(/ŵgïžÇêrtÜ2ÖúÔÀÖÈ~É™Ãků½bwãgŠq¡|jhÑv_É?&¿ãì¾íLs8j˜;€1|)ïä’Oi(‹<ù”TH¬¤&‘ã¥æV&nºFAÏ}dÔŠ*pù&G ÝÖq¡qñ6—4îÇ ,«X¿±‰‹5å¶ŽÖä“ǘP ¯4^¸Iòª ØTzó;§çkêþndTxš6ÅÖ¨ÚMCµáÎNƒ[ÀÖ§†BS¾PʨÃÔ€ÝÆ2;`•§„LåSC‹þ³û¢gô`à]Ážä¶ÙßKßü6G îßSÒè—¦RÑAò#Àî€Lù4ÏÒ`_ñ_1};’,Ê}·›£z¾z!è¶.½å—‹hðIï¸Phë¸K¥MìÜÞÊež»+¯–Ä8î¥LsÝ>²¼ÅÀ‹Æ"¤)“PŽMËŠ…á ndڑϵ`[ÔÖÏ™\½¾§Á%(ådô©A >L ?*0}a{Ž1ϱ¦pjÈ)¶T=z„qÀ)ÇW›™Êuà /—Ë?uJ‹wWiù0]¾ûC|+±ªØF‡¹‰IŠ–‚’ûˆƒµ¢ \¾É(A@·u\÷·ürã?ú–m\(´uÂ{¹+—Û[yèC:²Iå«„¥¬Ä‡Þ¿¬sìÉ#*•tôD@ò§åÔ72 ˜›ŒbkTíÆÁÚlw§Á-PëSCjðae’NS|˜\–T`P.œZ”/Ù7<<…×,Õ [Ò`»LpÏ©3G&"2Ì ˆ~ÜH’c!'ïæ(‡Œ×Y[§DÿŒÛ`µÐÖ…ÛÛë"x ±àö6Gås¤W ËÀÑÏéxó‚°Ï™òáÆ}i0}ŸmÙL9¶µ’F±EíÙ¹© —8 ŽPœQ˜0§ÄšŠUˆ™ðIˆTm0¤«…SÃ:Wí"y~¸)ˆ³´j÷5„QFwØrPdxI¼àÌïRšµ¢KußÛ¹Xt[ÇVb_á{غõ²â„'A§¼PnÈ0iҕᨦžÑ8½`ÁVóHi¼jšDTrWîF¦ Þ*a£ØU»jhvvÜþìÔpO}O¾_žåñŠÜ_þßoàÊÀ3ÌoÊ{åNh0àð’q8aô÷²[`œÝ7 QÆWê¾K¹<­ï_OóMÂÍÑ$,^yxt[‡½ÂÖaâ`§©Ñ‹å}KW«lñQ¿·äÆœ?.a®ßIüÇ“b÷•Oni“»fî²)H%k»Ôo£e´âOQÛL=®¥{ŵÝ\:ÆõrNƒë1ûr}j$L#°v°¤sÁ \55|©ÐÒ%èz˜ÈNo¨Q`ué#µÇ¡qª Y¥Àª¾[¡<3o,Š^b? qÑôXF­hÚ/;g  Û:^†@ruàDäÑÕ}KWmÄñžÑ£Á.\ûZ *±6¨:¨\dUQÛÌ"O6b[£jOA‡•Nƒ[EŸØ*ð¾ü>ùü7 審¡¥Sq_X"S¾‰ˆ`š 3ņ™"¢&ÐZ0”¼F!\¹€—ȇÀëü3Üh³à¥¡qú;.¸9câ5—€€nëæ‹U(å¸r²¦ÑÖÉKg“CóH`ÊX&ebe02W¸/–$Ô³ 0ü(³`AQÛÌ‚8š2Š-j»7x0” ®: nSŸ`Äʃ¼X&.BÊ““‚T6N -ŠûŠ¡Æk!5⾈«Qlí“ÓS|L‰5Lå åò¼®È@¤MÂDZ”>µ¢i½ìœ€në¢)[56xRmÉ6ÞÄM.Q¾<ÀB€~ݱLZÃó Iƒ@˜X©ð¨èôèTl©rNmÚw#³ÈãvŒbkTí1þ}Ö8 n—©9»Šc¦# o%ǹc\èC8™âƒÂŸ½ÿ«zªŸ0sû ýF ø- ¯rçB¥…ryB™+ÓãJnd§Á)&^v@ ÐÖ¥™p0q›¸XÓhërnÕª|àt0zÛîƒÙ®oxÆÓ"¼kœ9µ9–sž5—6b‹Úñü\œ‹mÙipËÐN Øy™bAV•·ä§†–N¥û¢g„8…ÌQ˜Í<–î»`9¤\¾z /q£e½ñByHu`×§?îVp )µ¢J|“#P‚@‰­ƒ1’,‚ËS2áF—HzÇ…F[—ã“UùÀAó=ÀÄ_É‹lü®–SMÜÈ”œçÉÅÖ¨ÚçÑö{9 nÁ¼djÀì s#½¼F}OEç4˜ÛOü!Ì9§†ÄÎÛ—™ˆ r ±0±â¢ÑÛ)”g è}Œî_8°qáæHGÞ·B[Ç3/h0|xLws5+Ñ`":ÐbAÔ– bü•"ܾÉip;†g´`Ô€£vÉ9| ¾ 8 n9 §†ÿŸ½÷yÙdIî{»ïˆþgüh«McÝ…ì…W³“Œ³0÷ôÊF 0Bw„$ C/Ä ÚYŒHæJ -„‡‘¢¥Ñ˜™á‡¹Z ŒŽg<=¢¯|î'Ÿx;OuýˆŠ¬ÊªŠxÞx)ž7+++2òY‘ßÊŠÊ*öö‰"~!À DÐÉ㶃††=ò®LÔØõ´”—ˆ‹UWԋڱʒ‰À,M¾Žˆ/Y²þ.9:òwÒ`%Èv¶!K™ÜŸC€EEít2K6ÚŸÛ jï·×9’ïÁÙ84ÈÜoyà~‹13ÃÇ {5<—ÐÙ«IªA5¾ñåg0è šÉYMjC€KÌ6OEKÔñ;mÙütG#»äîA É׉£ã)XÝŽðuËÄ­+‰ö[aõx{oßL½Š¢v:™íP¯[Ô>°®v÷Ç“ï1±qhàU,Æ…évÄа§9ÃsùX‘rpu&®IÈ6uæÃSŽKó9 Ð#€AVKûô{M]¨ÄŒÊ¨Ò j3sÅÌ<¡òF »#™ÃÝ ^tØ„L'0ú:¨¸šŠ¯{¿,¤$úø:øäû?80·®ï÷\ÿÇ…‚ž]Åt2v¬ZKÅ6¨Ú­Ö¹ª|Òà=ȇ"åxgd¸ñŽÑ¡Ïа§ËçÉüù@†¶7…¯wU‰ãEYa…4™8jA«Ú ¦È©ñÀ$Ø•Y¦‘dÙMw4 KfÞ=F_Ç%‰»“!ëÊìvñuÌ-×0~YÆ9ì²h.ÍyØ„ÿä‘Öêó¬t2Ç™5(¶¨³ÁÇõФÁ{°µ 2:ðËÃ}z {ôWÎå¢cã½o>%iYS—7;”³:Ä8"3ÀÈ'’ÙudòŽá¨ÆVµe0­¼WXqjGÂÙ êE§ ÉœD  »¯ƒûñ¢œ„C•!¹l ›})x56X¢$˜–C,\Ó 2ñMM8¿0·Õl௸y'Íó¦Õô d:™ã,Û jgǾ’“ïÁ³ih`*@F~”é­ {Ô¶Ÿ ¦R"ʸÏ´ŸÞ«$ãH™ˆ~û†‰†WödD˜ùlvµirREŽŒ­ ÕOw4 xfÞ=F_WBŒ&`dâÐÈç9Ît‰U_'x¸9-1Q·ÇR¿îi°ô X½¨,¡xNI'sÜÕ[Ô¾dä=ή$' ÞcãÐð¸û‡£#‚„W‡†=jÛÏeu2f­¥|àÔ5uíb[KþìOýchù2µ²$§Ií²K<}¹<¯$Áî’XòƒzQ¥Ey(° `ñuP\îR¹ˆp\GlÌ‚²K&•$1uz«¾ ‹÷J‚_6y8eÑüò2ûr¦²Áý­ÒÉ(àì<Û jï4Öi§' ÞµehÀí3ý;È™}D(ÃÄêаGmû¹CwÍ Vchíº”dTe-#Ö=nŠäVµe¥}æšH ÏV‘îhŠIæ<Œ¾®8ºgÏkÄÄXrpk²`Î< /#7ų‡¼eâ»ês+f?¸5Ð5L'£ã³çhPlQ[˜ö`’ç& ÞÓš††áÓÀŸÏ؃LßsõDÖØ`ÔfWQ>¨UZ”‡ F_'«â,t—4w'áF¤÷Ó`®V6|ÂÒzà,ÃfiÔ° qVÕ`wxtWz°À…ÜÎ#­Ö5•œNfŠI¯œ ØU»—ÕŽ–“4xÂÆ¡¡<Êöœ ‚:4È£Cÿ³Á{À‰r.¦!T›ƒÙ$.BÑ<Ý‘NºcŒ¾N>”É5Å#0y‚4îNžˆÁ‡÷Ð`¨#©,¹À5»´~#ªRcá±êà#c ýÂÙؼ‹2ÄBŒx¯ÅÍÊL'3 K—̠آö¡sA]°+$iðÛ‡™!Á¯Ò™H”íåëéˆPsœEì§ã¹¼ÇðÇ Œƒõ¶.U[¼xÅ¢Äu]bElP/ª´(%Œ¾Nžs=º8qtõ÷Å«,Q »¯+sË·%¾ Ð"±´~#ªÊE ÿÄoà=Ft¶½œBXoXüÀ¬„¥LAm«&„:s_0]èF$¤“YBr~Plƒª½ß^çHH¼gãÐ@8DyÇù}00œ]e*¸ihØ£”seЉáddL±Œk–Ö!õ#ªÜqëñÏéŽ,¨f™ûCÀèëä͸ÙßYÜäë¸Të»\§ìN? òDø³A5Y¬¬8ÞaÖƒdáÄ¿ÿÎ_– dbÞ¾©í7%3šÜ<¡ ”å“ïǶUBPŽÚ9Üjk{ù¤Áv¬¦%C®ž­Ž:–Âö’©V÷—SVLº}A£¼N„ÉË×½|‚ òȲPëwL)õ¢J‹òP"`A É׉3þš|ÝÛ7\ò8º~øðvgW£d½M&ã¥$›â4D8³Ç,G#i ,–2µ^0,šÜ&´ùezdöôt2³°tÉ ŠmPµ»˜ì!Iƒ÷€¬ t]ÂØ$FnöW&LCÃÕCË8Âë02šÕ“zµ@ÖtÂRlŒ}ŒSŠätG 8yèŽÐ}âÊV­ú:&i¹0!´<‘Êl-ª`ÌNì·´¼îÊE ­%ÎùØÙÂ’‰p.|æe-1¬”o:„ªÄqÉ 0Aòì©:´©¨t2SLzåŵëÍT/(RNE ip…bCBf— .³ï7e€X6h›§Ì"©f`b”d“˜ÃÙb’Ô‹*-ÊC‰€Ý×)®lõЪ¯“U×ê²á2[[ž -ܱ²F1´¹ÜÒÞ–/†p®þÉW-xC–ˆÙxB´z–±€8|¨¸Ed¦“1»¡XPlƒª½Á@—œ’4xìúЀ÷–9yEŽéüó0jN V‡†=jç¹ úP•îH.Ý1º¯S\Ùê¡U_'ƒ¸0‡·®0áYÀ˧:n+7=6Â3ÃTå9yB4+|C¦(c?1Œ«Ö’A±Eíœ nµµ½|Ò`;VÓ’–¡á“OxÇ„¬xuD¨V‡†©2™³ žŸÚ—žêE·!“g%‹¯«î«)±Ù×-±\òåP‰¾½ïÖ´rg±éAK‚éå¢É‡¬1û~¥ÒÉ|UϽ ØU»§åŽ”•4xºÆ¡Aбn0t‹÷ãê¯2Xlö4Çó¹uÈAª‹ªŒDl²†g}:°†u¥;¢‘駃€Ñ× Æ¢»utÜ»lçû:Þ¡“h Xý F)ðîWðä5ý3:MvÇÿïÞ72s¥ˆ&»êÀQ;gƒ»t€Y!Iƒga1fZ†&ëZšâëïùCƒ±]ÞŠ1aËØÄíd˜£j Ë·ÃòûòuÙ–#•ƒzÑ.p¥§Œ€Å×áи—»~qnœ5Üõx ·üïdñp !¬U±¯¤•ÕÒnáÁ$Ê»uên™«›b„F”wž=×ïˆUaÿE^ yùš*H$ þ¦Sv‚:ð jŸbÒ•$ Þ¢qh(+öÜ(Vñ0®÷Ûì  ™ CÞ9WÆ‘ßøõ_œ‚¡ºö‘½MÜ_³!–`BI—ÙVa±éŽìðfÉ{BÀèë$.— VbÀ¸^Šëëô© žÚpyrS,kG@\—æ~™’„FpuÃ6e]ÄúDiz–”¯—¿¼1ÇYÓ’r Öðj9‘„r J¼̧uਲ਼Á;M¯œž4Xgõ>40Ôx`¬ÆnIƒW–@ÇD æ3®‘à5㉖b ”ØQJòHTçØA½¨‡,“(è¾nèʆ~üÑî°¤¤í·üò®1L™\§lK ïe:—S`›$(<»È°Hò²1…‰ˆ€º/ oͧjÞ‘³ÄÆúgE¥“™…¥KfPlƒªÝÅd'I¼d}hà!>ï™» ˜ÈñK˜¤køÜt\ Ç>4ìÑ?йõ^¸&z)ÏxĽ‰HÃ(úð—î¨ì)'º¯9±Uê;,o÷uðFÞ\c½œ åƒà¡±¸DþCƒYýŒ]ðRþåk¦—¡¬²À/3ÏJù¦CTca‚]<Œª ’ÓÉ4ÁÛT8(¶¨Ý}àkÂí¾ ' Þc_}hÀ½ãTñÃl$¦Ûp,¥íCÃýýŸ;íóÁÓ~Ã* ÄÊØJA½h?´RÒE@÷uÕ}QŒ‡þ0IÉá9‚ùÉ©¦‰ƒ|¬¨†×…“ãy@·ìš§N¢¼+w[Û¡£Vðj©E2»Šð ^TiQJ,}ñEð=6ño£ÝY§w¯cFºÜÕÞÖ &ÁîJ3ß=DÁœ- ÈðJyóaD!Ø,’Óɘ¡m.Û j7›ç¢’ïÞ84Vcƒkx0™³ƒ‚d44ìiì%ç2y"« ~ (åmî~o¯Ð(86£6¯Ò0dó¼J¬Œ€éŽ.éYéå}âÓ–äëäö™«ûñ+ÌËïÓ ¶µËRfe5³[4o/ÌEr¹×¾­Ä®.9ŒŽÏž£A±Eíœ ÞcwýܤÁ:>úQãÐPæ036X‡rù(—?ËBLÏžË{+‚,”Dåò* ’©EY()¨]F7$&Œ¾n‰ë*ùÑ`®hžq÷àW1;_.üúª¬Dó*·Ã&Èn…D2 ÈÒmEõF›“ÒÉØám-Û j·ZçªòIƒ÷ op€¸>üð(<øü¡aOc/9—A„š¬8:Dƒi#—³\׸BªôVsÿËêj”¤‰Ž¬Qä.áPb©‚Z–\M:ÝR{ŽÅ6¨Ú{,uæ¹Iƒ÷ mðó¬\Ç…aBŽö´÷ªs/Øx¸)qe¤~ŒÝE ma¥tGý€OI‘XõuÜà?:·Û¬ÜûKLXyvœ¯Ãipi—«[ÿàÎìQ‘x3<Ïl édfaé’[Ôîx_ÖÉ{’4x5W‡†Jt™˜nõè4qÜа§½WËxAXƒ)O9™ÕaŠii"eƒ†2R#Ó²°RP/º–<%" û:b ä:â‰?ë-ðôŸD%ÃSWs,¾ŽØÚÑ‚ÞrS<Ôpœ~û†€ëÕ„qÛþ'Ÿ4T?ÿøùŒÛ‹r8[žòËV L–¡aæÎeT+æÒùåa%3ê¬çÐKÿº°„0m–3Ò†¦;ê…|ʉ…€îëðW\ž\;Üò½Y dºù:žæ ó‡i‹¯£v¦ve­˜ú‹ä)†\Åx ò™×­q«÷Îr—Íúá"Ýqqò©’ä$ ž…åèÌ µ{ÍüpDùIƒ÷XMª«—€Ç‚²t#‚ã ÷hë\fix¬Yâ o›ÎT[›Ö´°RP/ÚŠI–OFè¾®?pmrˆë+a 7< –¨¾‘„…S†m¨ÒRP„DOAb vB(±ú²;<}”.÷Ú/^K,SÍrÓ=*ÓwWˆ÷¬Ìt2³°tÉ ŠmPµ»˜ì!Iƒ÷€¬ ÕÕËXÀÔ„Ð`¦&ä­ê¤Ávð§äÙ%Ï+!Ãö-%‡Âõ…•ÒYðÌ2÷‡Àª¯“Ïãë¸FÄõÉÍ>W+$¹:ÃiÂDƒÿìùCT™M½Tc}oïÒÖç’˜›: $Š˜@}ªyX~g:gƒw¸íô µs6x›Å-g% ¶ ´TfuhŸÏC|œ0Sü24àöI0ù0jŽehXÒê.óù³:ÌÕ½Ð×!p{ÂIMÔ‹ÖÖe"؆€Å×±ެ!~Œ…"ÉQÂ!¤˜Å×ÉUI¬ÂPùÑn=Tׯ‰ÖX_ªc¥ß*ðˆD¡Á/^ÍJN'3 K—Ì ØU»‹ÉN’4xÈ–¡W?\8ˆgsò: “•ôN–¡aæ±Î…©Ê‰ò··¬úîG· -ë—_õ/Ý‘ O¼[Œ¾N\W¨|NŽ[×.4X`e²°m‰ï¡%Zc}[ËëÒÓU2Lr>û—Nf–.™A±Em}~¦ 8OVHÒà=¦· Œð^Y:V¬?%d(I\íÂÀõefIævxpɼ:™µÀæ÷#e­à[ø" 6$3|+ƒzQ¥Ey(° `÷uÜWÊ“/¹²à®ÓÛüaŽÝ׉@„[–ø¦^âo¥i sœ«7³µ¼.mx´Ur:™!z}ÓA± ªv_Û'-iðlíCÃÐí[Òö¡aþ!Ε ººØ‘¬ŸVw÷4Ù6È0[Òåu¹gÏŽîhàyn\Œ¾®. QÜÜžÝpAñ*„âôì¾Î²Ä7ÑSÌ˽s vºMËJ³à·–Ÿ2›¹Yr:™Y<»dŵs6¸K˜’4xc¦qhP†€¥Cö¡Á¨jÜb\þŒ¤5|WF4¸q¯5viòú¶"<¨í…UÊy²X|_LæReã¾RžyÕ÷æö¿,Ìvxÿ;»Ä·¼ˆQ¦o<\žò”Ùã…ÙàÖòö°Yr:;È­%ƒbTíVë\U>iðä-CÃÑÕó“íÂÆ“PaÂ$x gxtgš!»Ò`""=éŽpòÐ#`ôuBƒ‡ÎMÖ‹ØOƒå©W¨¾Ä7uXóA"oåä,ÑàÖòvo–œNÆrkɠآvηÚÚ^>i°«iIãÐ0Œé¤ÁC´e!#"ÈdE#Æ—Ž„³É”˜ømExP/ª´(%Œ¾Nh0dUÝãË­Ïžï§Á(Ù´Ä÷°Qerxa6xX¬¦[Ë×WÉédVaÜ\ (¶AÕÞl¦“OL¼pãÐ`¤¾ÃbIƒ»(±»ÊY³‡%›åŽ[¿éNw4 cfÞ=F_'‘pNâÙß•{ùzèÜFé&_'ïÉÊ­ëìºÁ³†`&¹éãÈ­åg+Í´HN'3 ]—̠آ¶>0uçÉ I¼ÇôÆ¡aäö-»MCÞ&x>—8@^²þ|)³ÛÛ.|ï¸döøcªŠïF±ñn{•§sì ^´¶.‰À6Œ¾®|ÄíýWãef˜(#}a»¯ã¥³á’†+Ä`´øáhwŠÂ¨ÀhwZ¾%¯B,GYíE×þÒɬ!´ýxPlƒª½ÝNçž™4xÞÆ¡ÁÂ{GeìCÃýŸ[†ÔgÏe3¦•ØäU—¦ç›J™ %ÖÉ'˜PŠñŠœÎ±Ó)xæ¡;FÀîë¸leeHùewäÜF»v_Ç2¨ûÕïUÅ ¸ V-æ«Ê¼S ßSÖ­ÓZ^—öÁÑ·o¸×–›j)dXýK'£Â³ë`PlQ{å¦o*Oýä¤Á{z€}hyþÕ]ûаGççráÌÆ[6¼'i(«þ›½Q˜ÕÒÊü 3?·M–¡P$õ¢J‹òP"`AÀèë`ªòk?#[M“P<žÅ× …IB#¡”Ü«~ãË+|RhgY/‚ÙéÛÒmz3[ËëÒ†GÑ–ÛmŒM¦ÓÉL1é•Û j÷²ÚÑr’ïAØ84(CÀÒ!ËаGó@ç2óÃEL]Õ™N[Ó{˜ÉuI%I£ÈLw¤€“‡î£¯“u!)åmñ4I/9:ò-¾îÿÇÿä© ÇÜ“&ÐWø¤2EÛ䕺K¼ÜZÞhk4o}ÿñµ^ì—}›ÊI'3ŤWNPlQ[éê½Ày²r’ï1½qhP†€¥C–¡aæÎ…3pðî¹ýù¦±u˜U&™å—–º”ÓƒzQ¥Ey(° `ôuò¥xf_á«ÃmÉÑi°hˆWD I‹[Þ[! €×@b Í¥[ËNŸÝE&“ØúýwYZY§4édfñì’Û jw1Ù B’ïÙ84(CÀÒ¡¤ÁC»<.”d~¾9k”îHÁ3Ý1F_LJ޸‘Ô߉9=»¯ƒcWŒ厵ÌÁ¾xÅÕý·ÿù_³¤¡Æj­å-† ‰¦š¡ÄÚÁ®~z:Ÿ=Gƒb‹Úú­ÓLòܤÁ{ú€qh¹}Ë®}hØ£ såá&®@âëºhÎ` ée!b– 6 êE­Ëb‰ÀF_Ûdcò³z9æ?kz6a÷u„×ÖèVwa¶yI[ò),W78›Nƒ7”Wª–C%þáö%»œ<ÜÔçMœ›NfÛÍ‚bTíÍf:ùĤÁ{7 ³Î_Ï´ {ôzn×µŒÊÒ“UŒ˜·ùá¾3Å'ÝÑ“Ìy }¬–Æä§¬\w×êëˆp`“7ÎŒÈsû¬§ÊáöòõÄi!õæ½&¸‹×Ÿ7!'ÌÌ^9A±EmºS/RΤÁ#@švCƒ2,jšÔW˜ †'Q›·Kd;6i"«@8oúðÝ«i-A½è´!™“4!`ôuM9+9ÎÎCA±Eíœ Þizåô¤Á 8«‡ìCƒ2 ÌJ ø LPSy‰›„|ñƒÚªiìxºŠ‡áa¥Ldù·¡6i°Ã,y÷}¼†Oc îYÏ6Í4ú:$>W+/Êqm’`Wn“gÁç(¯Örˆ«›(bùR$™(0-ß*|*AÉa•|SÓ¸2¼¯§æPPª¦7ÊÉÑ ØUÛ‰ÑWÕH¼ ‘RÀ84L=ÿjŽqhPt»ƒCŒbLæVÇ’¡u+«Ft¥Á<3ea%¦‰ˆc„#ŸY9 b<ýKw4Å$sž«¾žÉ$w©5$X?å[rF_G4”\ø,r †Ü²«,ÀËQ>¼.¦¡ Î"Mæìím«ð&‹ËZʰwÜ> °é3{édšàm*[ÔÖûLYx„@Òà M»«CÃ*Ý]*`š´½ÂuÀêÕF%™æ1.Xÿ êEõFåÑD`U_·ôá !~KŽŽ|£¯ÞË$3|* ÅÕù$¼—b‚© æÿoð»ÜêrJ]kbØäVáÃsWÓÜY˜Á5î…D¾"·ŠØq‚:ð jgǾ’“ïÁsuhPœ¿~È84ìQ>è¹…¿|ÝQyFF¦U,5¦;êˆ|Š „Àª¯ƒìq#)E56¸äÇ{›ƒ]òxv_W¤ÝfS™[^]0Mîmù4dQéý¦PÐ&á­†«Sy5¡HH'£€³óPPlQÛÒsv‚ódOO¼Çô«CÃ’ç_Í· {ôrî(°¯C`HÁáÝèR)Ô‹Î40³Œ¾NÈ0³µ„Âʯ¼%§x<»¯ãÂ'8Š@)fta­³óºµMÐoYT覺գÓÄ’ðY?0=½cN:™Ž`ŽDÅ6¨Ú#ðÝî& ÞcãР K‡ìCÃý£œËÃͲäË¿}C‚ÝŽš3kT…K,AL£¼Z>ª(ÝÑÜ}"Ø}¼ÙÊe%ӶïiL=^³¯[¸E¢ËÍ2/µõõ6#%gwÓÉÌÂÒ%3(¶¨Ý¥?wÁðþ„$ ÞcSûÐ0uþzNóа§îÏ•'¡Œª8»¾"Ǭ‘HF¸$$ÊqöUš ^Ô½…SAïX|VXA®Öá/LXqwv_)e`a×åÍÖãÿd™šãëù †t2ÀÑu'(¶AÕîjº…% Þ®ehPü¿rÈ>4ìÑ?ʹ¬ÂÄ’Gòˆ³,”ôâUGÍ£yoE›’à…nƒæç3:"œ¢î£¯ƒ£B€Y{Pî%kÀ-7•KîÎîëXq‚kŸ¸Ë$3À‡g?øÈU,‹S†ÄôK‘£üèÇ?¡9–’Ë$çéæHTPlQ;gƒG¦ì¸›4x˜Æ¡aÉÿ+ùö¡aþ!Îe8c$®ŒôÝO¿ÝQs„× ÃO>y\ Ö=3Ô‹v„+E=MŒ¾Ž«‰k̆‹ã–œ.4X$ þ0m$“fMÃí-‡X ‚‹Z&‡Gé¤ÁF ¢ êÀƒª¥W$ Þc)ãРÐÝ¥CIƒ«]d$âa+¼<£d¡$µzt‚±á"‡ù%vIsë={÷îh?à)!"F_'ëBLt=4X.(®©?úø/÷û:¦‚ea¹8IŸ½ceÁ4Q†£$Ø6Àž4xhžO êÀQ{v<ò u Ý’O…»faš?Í1 Kþ_ÉOKqŒ9|¶—HêÀQ[˜.ón*M<5%ýŲpûa™Ð °m¶Ú‡…ñβ Sýï2‡ˆˆ¿ûÓ_f#1k‹=­ ×åõ¢z£òh"°Š@“¯#ꫳßê÷ì¾nõ.uµ­ îúB®Et2”¶• ŠmPµ·Ùèü³’+˜ã!ÀxiáÃò¶Å°|ÓÐPݾ%a†úÜkºF–Y¦gÏk|`—ö6 OwÔó£¯#d—X…é¦8½&_ÇšõvøX Y¥üöŽŸ6ÿqÃ>½g_ýG:™Í€¯ž[Ôžv¤ÕÆf#~h0š‘ëaƒúŽ6:¡0a~%XB¢ÑŒCƒ2,jŒ¶ZŒËŸ€@âxç…ù¥'üìùêPbll«ð ^ÔˆFK–0ú:!¸<§Û’£#ßîëšîX—²”ÿã¿ýjõ*¸÷.ïá¢p}ýVêe˜[ýG:™%íÏŠmPµ÷Ûë Nh0n§òÌ f†™+6 ʰtÈ>4œÓO.¬…¾Á¨ _e¢I¢SØ•ÛýZ‰päÀ±-ÂÓíÇ<%DDÀèë˜%àòd½Aî[‡Û’£k¢ÁM·ÃD:ñ&,L–; ;ÔWØ/jKavYˆ˜ÝÙòM™¸qÁwI-FÉédš@n*[Ô¦5µ4 ÛpBƒE xˆ‡Ùà!ÅÂqᨄœãÖ„, ÂÆ¡A–% ®}ø‘©¾{¦*ïnsR ìIˆpF=F+,»*<¨ÝQž›€€Å×L8’Ä’g›æ}]]´Wl¹c¥š œI°á̧¦”O?‹phöïütÙHP~Z¸5ЪL»ät2­8ÛËÅ6¨Úv»\[Ò 6.Svb C߃áNù%=¤Ç¢ƒeh˜º}KŽqh8ŠËkar‰á•…áµ|­õåëŽ*5 OwÔù£¯ ·$²Z§gôuÜù"|tÇ }]ÂP¬ †ŠaE–[œO£+N° ‹ªÕM"¯–$Ûó ÷RÅ’°HN'cG¸µdPlQ{¶÷¶6?ËÏ"4x ÞÏ,Ó¿ø1…œ‡eX:d¦Êßeƒ‹¤1–AƒÝJ:6³ ÇÐŒY„+ƒzQ¥Ey(° `ôu56˜›Öá¶äèÈ·ûº¦;V®kY4˜Ö‘`×þ÷øÊ~ÂBIšÆ6Ÿñ'¿ö3Š»³û:$#JîˆI,ùÇÀŒgÏyµ™4AåkékNÎŒo!†A>×¾JV+2« ª."|±ý.¢Ÿ’NFÇgÏÑ ØU{¥Î<7ið´W‡Åùë‡ìCÃýýŸ lC=-ï˜ Ë÷M§;ê‹gJ‹‚Àª¯«/Ç=²¾?¢¡¸;»¯+‹?<{Y•ûbÒ³è1o•åEZ^©“™XŠéÄœ‚´o}ÿñ‹q$ØÅÕÌÊoÍD”‘X~ù$´.!ŒŽÏž£A±Eíœ ÞcwýܤÁ:>úÑÕ¡Aqþú!ûРký(ãQ›¤-Iƒ£Û4õˆÀª¯ƒé}õk_/žÏž?œ!óŸ /y<»¯c,|`ż{ ªuÛ¨ý‘v.Dä\A ¡$Ë»‰iþþ;I+ºY‘¸†1Kø±Þ‚R5½QNŽÅ6¨ÚNŒ¾ªFÒàUˆ”«CÃ’ç_Í· Šzwvˆ7_ˆ¯cÌb„ºªi鎮B>ë½Ý×ÁrYЛkSðпAÉæŒÒF_'|²._Yhð‹W³˜‚+±Ðòï~úmÞwƒ/9  þ„¬)SǾËî¬ðÖÌ2ÏüâU}ÁA‰å¨’ÓÉT(º'‚b‹Ú]nʺãy“ï±£>4Œ¼}Ó®qhØ£|¸sy¬ÉSNævVŸ*×´ ^ô8@RòA@÷u|\2 ulòrRØèëäóõ„ù0û‡ª0Oæ¥ö§~®nK÷Î%þ¸ËTvÝúÝkoŒËª/8,iRÛRÛXs2Ñ  ØU»—ÕŽ–“4xÂúаaD¨§‡†=ÊÇ:—éy5†Ä…š§;ºü¬úBt_W˜ç³ç„7}F¹‰Óvn„—¨ïŠ1M…eƒˆÂo‡ej:]W`« Î:è^›ê˜¯µÏ&ÒÉÌÂÒ%3(¶¨³Á]:À¬¤Á³°3õ¡¡rÚ ‰¤ÁC³Ç û8Qóìy á–9'Ô‹žNÖrÇè¾NfƒyôODÑÓMñGû:nœ ¦b2ù*ëைӰמNÆŽUkÉ ØU»Õ:W•O¼y}hP<ÿê¡£‡†=­>ÿ\bö˜B‘¹ì’¾ä/ÝÑ%°g¥—# û:¡Áå^uaS<ÞA¾BΛnüŠJå^€áí#˜,%GË[r²­é‘Nf ¡íǃb‹Ú9¼Ýêkg& ^CH;® Šç_=tÐР5Æë1‰ ¬N€ãïË\¢oP/z VYé=! ûºÏi°ÄÙò ®é¯w„¯cA`Ñ`n™ LBqÌ]3]\?Ìñ¹Õnßžûô{+1 Ÿ—Ÿ¤X¸˜àäų祌>T7)øAF:™àèºÛ jw5ÝÂ’ïWÏ¿z舡aOK/€Ër4|­’˜¼%yÉ.™ fÅcIÃÉ©k†rD¤“€Ñ9[Ô®AIqŸ}–4xO/ЇÅó¯:bhØÓÒkÏeÂe„"ÁÚ¡WéÔ‹^WÖ{7辎ëvwèJMHÊêjx ´b¡E""ð!Š8*+9 §¯yÊÚ%ø gzµ¿+4X¿‹O'£Xjç¡ ØU{§±N;=ið¨õ¡a•ë*’íÂ`õ-ƒÔíR]Ʀ¡|{:Ý‘«,é–áeÉ_}rRZ¡û:Žrm2L,_%湿87ËÂùºJkKXÂmåÞUsàXdΖߎ“oLW 2(£k’NFÇgÏѠآvǹÀ»<7ið³êCƒÂrW44ìiìµçâd„ºÖõ¢×Ú.kw‹ý™)\6\/”)z꾺+ñõ“ÊD0Ëjòý8 @y”3u}ùºê1ðeÁ´5ò©´}ç!Øu½s‡ç‚i;ñÜszPTí=–:óܤÁ{ÐÖ‡†©Ã·ç44ìiì…ç2[EØÞïüô36–É«ƒ´Mwt°)ö* ¥W¿ æ——Ýfß?5ú:h^YØP^‘»½õ壯廯·Ý©<Î×á(ˆˆ`}á ÿŠË’e"X;bí/ÌBÛŵ¯ÿÙŽx„3“ï±’qh˜zþ՜ㆆ=í½êÜ2†¾x%Ÿa"ÁîUšõ¢WÁ•õFAžF€M&‡ñ?ÌâG^»¯“™aæ< Çê[É™º¾ƒ|Τ4·Ï…_õ÷ö ˆ&ü®j’Næ8CÅ6¨ÚÇÙ±¯ä¤Á{ð´ Sç¯ç44ìiìUç2@3ŒòÈUµÊìÊÝÝ ô#·Dààºtiþ˜–_23,|˜]ãR³û:¾A,°ÐJ€™"f¢x6Tø _Wjùšp6Þ,`÷ ‡Qê¤êòjÃË×_ýÚ×-š`ˆ«T½ûzƒb‹ÚÃ{Ò»7ÓÉ L¼pûР“Þéу††=½ê\Y0G·¢€¼^wOÖª¯¥B6ò7¸x/o·Ib¤ L˜ŽjôuaÂsÈ~™fÉ…Y,~ï_'^â[ߌ„ J³ÁG;Ñ'¶Üt$Œ&ºûêëdŽn`,ùA± ªv”¾‘4x¥ŒCÃ”å®æ14ìié…çrÌÀÁü’Ìˇ•¯º5îëŽäêcp¼d&0+MèõÒæšÂ/áÓ„ã‚Ø­šÑ×=¾"÷âì—¸…ýVx„¯“{g– –ÖéëWŽHÀxq_ø®/þÒ—¸ÒåÙ–NÈû:™#WfPlQ»^‰qÁw«yÒà=¦1 ÕáÛG {Zzí¹Œ#lÌJ¡ ž®^¥O_/êäê» Ì¬× ôà !“–ÓéJ£¯“•¦ìwšSáA¾GÁÚåÅ´Ï>#Bã•"¨š{¹¹€—»øµ5+ú:'}̉A± ª¶£¯ªád v¢Æ*\£Æ¡¡:|{â ¡a¤”]âùÒ“<â$qá}q_w´ÛGé6©ç*x$x¯LÿÖ0àÙ³Œ¾Ž×Wáœl’¨»5sêòueÁŠ[8.ÍyâCfÛuBfyá¶t÷@M„°^i_'£×õԎŵ/õ8ˆ¨ÑjnãÐ0õü«9 ­ ôVþ¿÷Ç|ê3âCiÞ׋íöa›bÏG€ùI\ îhu5úººn°<Á™þN]ßA¾Žˆ+íªËöž°ÔÈrmrÿÎûz«Êôu2W5Ùg½A± ª¶Ï>0ÕÊÉ@ìD)>zŽqh˜zþÕœƒ†½9n²?kûó+£*3KW©Ú×íöWŸõ^ˆ€Ñ×1·\.Ò[lpY0)Ù[DÁìò;u}}}a·8ŠPDäN3GeŽÝ}÷ ß‚·ÜÂ÷u2Ƕ+šô Ø¢öêj4S8Ò×É@ìDVdž©ç_Íé;4´¶ËUyy©„ÓxWÎeÁ4ý“ãôïëEƒvûãàMÉn0ú:úÃ{yî/.Ž0‰P<^__GE(@,îpãÆ™Ì«°åVY=ƒ¸ˆ¯X@ø‘ /?Òêëd®jµÏzƒbTmŸ}`ª•“؉S|ôãР K‡ú z+œ•W­å—;â _ú¨¾î(h·wÞaR½#0úºŸý©Ÿƒpò"X^‹“Õƒ‰¾8Ç×á(øÎÎÏÿÞ·QÚ)ßÜ)Ëö^GƒA€™pb!@%â¥1ÄxéëÕ}Ì!®Ì Ø¢vÎ×ëœ ÄNÔhÅÙ84,ù%?ipµ…,¼É#E2>ÀÄÄcJ=zr¢¯ ÚíOÆ<«ó€€Å×Á{%6Ž'ïÄ æÊ=W¬¨”@\Ù…B\øŠšÈ'HP†yr&ÌI&UÛa¢¯“JÎtPlƒª¥¿9ˆ¨Ñj5ËРp]åPÒà¡-˜Ò‘GŠe®éÅ+ÆÖáÑ3Ó}ÝQÐn&àY—Œ¾šÁm³!ÁÕûáëäáÑ÷¿õÇÜDUEÂ7¾|™ÓàÖ@–=G‚"ÊJ»†Yãöu2³U<Ù̠آvÎ×i ÄNÔhÅÙ84T‡oO14´¶ÎOy<Ïec†&|•n}½hÐnøYï…Ø}_Íx|9îåkìê~ï _WtxöÚÉVBs¯ ŠÚ«ì–œ—¯—ˆM_'saŸqXuPlƒªí°̪äd v¢Æ,DJ¦}hЂéу†¥-Á‡¯úë뎂vû«ÀÏz/D É×±,óŸÁòKœðÔ¿ sòuÄârËÌ2elPñ 署w{E“ú«,›Ö×É\ØgV[Ô^ºirr8•œ ÄNÔh5_ÓÐ0ôü«éƒ††Ö:)/ƒ©eúzÑ ÝÞƒ!R‡“°ûº,¡Ì|rý*ï_wá½3U—À ¾gǶ¼@D5h_'SÅf‚bTí(]ÎÉ@ìDV«Ù‡e˜=tÎÐÐÚÞó˳Ö/•|åAÞä·Œ)ýõuGA»ýEØgµW"`ôu\§Â~o_Oƒ˪§úºÛg”2"„ˇ•/úcq‰ˆpÞóÕéëdôºžÚѠآvÎ×W ÄNÔhÅÙ84Ì:=3i°Ø‚—kx¤X†T~o_G%qáˆÖ׋íö­—I–¿,¾ŽÀ„ý²Ê7ï…±Á~åú=¥8'/Ê ò\h(v•ðWeŶ—¯‰Í ±ú†o_'sU«}ÖÛ jûìS­œ ÄNÔ˜â£çX†î.M,Ès ÌÆ§©x½ZÒßúþÑ~ôãŸè¦9èh_w´Û„mŠõŒ€Ñ× Æ­ &!Ô8—%‹¿ü _&„(³XÏȼŠËzDãÄA–° S±u_'£TôŵéÒOÐ^ç4ÙÉ@ìDVÌCÃ×Uò“mÁÔ’óé÷þší*ŸÐ׋íöCÓdú‰ `ôu°_.O &ýßþàwÙe;Ëó#ئLÀ>>Eº=QºÄRÂ{‰ãúâ/} H€†~ß×É\Òj·•Å6¨Ún»ÁH1'±5Fà¬î‡…î.J<Ÿ‡‰4(ŒwöÐ CCkc•¼~²V}ÝQÐn2æYZ}”O™:½|7Ñ.˜†ù¨›wYÊXY1X Ý×Éxè<~tŠ-j׉ ?`Þ&Nb'j´šµuh:=}ÂÐÐÚØ ËI#ÖŽwÀ¯R¦¯ Úí¯?ë½p¾Ž÷ã†++²{zƒÛvT›éšôu2z]OíhPlƒª¥w9ˆ¨ÑjµpCCk¯-Ï!ÏYlSvY„“Ý«ë뎂vû«ÀÏz/D œ¯ÃKüɯý A¹l¬q¡Óදew_}Ì…}ÆaÕA±Eíœ >®;9ˆ¨ÑŠs¸¡¡µ×–çm8FYqˆKÐË*ôŽh}½hÐnm¯ÈÚ/A œ¯ÃKüÏó [Y5âº{gªfé`q_ü®jÒ×É\Ò[ÜVÛ j»í#Åœ ÄNÔ³ºnhXm‘«¼kÃF(Ýo½ùQÝÈ\GŽkE_w´ÛoJv‹@,_ÇÔ+^‚wÓOfÒ.|¯VVÏàÝ^œ¿«î«¯“qÛ£.Q,(¶¨³ÁÇu'±5ZqŽ54´¶ÎgyY‡ó*ÝúzÑ Ýþ*ð³Þ ˆåëÄK°:™„"|öîЈKЃ½°Š2ìWj·¸¯¾Næ’V»­4(¶AÕvÛ FŠ9ˆ¨1gu7ÖаڜÊ8’ë‡0U*yG„óueѶgÏ/¤’˜.>3-}g¡¡÷‹ä<:>{ŽÅµs6xÝõsðO'jèXM†¦M”ó8±óÙg:„¾^4h·ÔgRÕ^„óu²:™S•P„µVôj$gEÕC¿±ê¾†…GÒrw'A± ªöNcvº“؉­°‡Zè¤<˜gšòšIy¸ùîá*Åúº£ Ýþ*ð³Þ ˆèëðŸ±XÛ»‡ × ¦êUê;´l_'3”œé Ø¢vSJC7!àd v¢FtŽ84´¶ÑCùòå¯~þ÷¾ÍF‚Ý«´êëEƒvû«ÀÏz/D ž¯{ûF–a‘6V\¼½¦ªû:™¦ªï¾pPlƒª¥;9ˆ¨ÑjµxCCk ”gJ‡¿ú¢7 vË<Ï}ÝQÐnðYçÅ„óu¼˜VÖI{ùš…H°{1‚æêû:sµO¢`PlQ;gƒë Nb'j´ânhhm ‡òònuå½ÂŠÉ¼D·¾^4h·¿ù¬ôZbù:y1÷Ñ~ã×åãÿœ8aî¯r­†ëëdZk¿ïòA± ªv”¾äd v¢F«Õb ­­sRž»`†°ï~ü_Dì2À]¢^_w´Û_‚|Vz-±|]½wþâ/} ,÷Îpãk14ÖÞ×É+}"Å‚b‹Ú9|\u2;Q£çXCCkëü”—5çù"ª|µ.Ây¾†}½hÐn>ìYãå„óu¼DÀë´}ôhp¹w¾h¥ˆ †ëëd6(pǧÅ6¨ÚQ:’“؉­V 74´6ÐOy>G€‰ ï‹ûº£ ÝÞO¯HMNC œ¯+/¼xÅ“£o¼ù&d˜áÓ°ÚYQ_'³S™;;=(¶¨}á¨wg}`Ú'±5¦øè9ᆽ9nþÝŸþr FIv¯Rµ¯ Úí¯?뽈¾N¾Ãà«Ê¤£üõu2QZ}ŽžA± ªö96Ý_‹“؉­xFZÛ衺Sÿ ¤¯ÛÚ¶Sú:™m:ÜëYA± ªv”^äd v¢F«ÕrhhE,zù¾î(h·nÄÔéë6€¶í”¾Nf›÷zVPlQ;gƒë“Nb'j´âœCC+bÑË÷õ¢A»}t#¦þH_·´m§ôu2Ût¸×³‚bTí(½ÈÉ@ìDV«åÐЊXôò}ÝQÐn݈©ÿÒ×mmÛ)}Ì6îõ¬ Ø¢vÎ×' ÄNÔhÅ9‡†VÄ¢—ïëEƒvûèFLý7 @ÏÇÝuÿýoð»¿ý•ßDì?Bì 27(O± €í,ż• ª¶7—ôq2;Qc ¥¥ü¤ÁKÈÜk~_w´Ûß«q³]w†ÀÇÿyΡݙMw6§¯ß©ŒýtÔΞl‡«µ¤“؉­è% nE,zù¾^4h·nÄÔÿ‰ ðÑG_ø›oþÕil6Ó‚@_n©±K™ jwiû Bœ ÄNÔhY7‚òIÔÎÙàãú’“؉Çᜒ)Ùí§˜dN"°ïýÉþçtuZ8Þ ìÝœ—ß 6ÄÉ@ìD ‡J•îìöwlÜlÚù — –`àQ¨0WÜùZe~ˆKƒs6ø¸^äd v¢Æq8§äD`Š@vû)&™“lC`´\p]¢Î“È„·a{7gÅ¥Áwc‡ q2;QáR¥;F »ý7›v2Ã傇 /埬^Vç¸48gƒë?Nb'j‡sJN¦d·Ÿb’9‰À6†³¾C¶#WY=Zg‰·Õ’g…F`Ø15$¨ÚQv2;Q#ŠÕRÏû@ »ý}Ø1[q9z ð4før…SKÊ'Q;gƒë0Nb'j‡sJN¦d·Ÿb’9‰ÀF˳;܆4˜iá\@xÂ÷qJ\|øûl…“؉>m”ZÝ+ÙíïÕ²Ù®3-\ã–¬«v¦zY—âÒàœ >®9ˆ¨qÎ)9˜"Ý~ŠIæ$­Œ– ^b¿5ŸÉáÖ*²ü} —ßþ>[ád v¢†O¥V÷Š@vû{µl¶ëLF1•î* .½35̺œ —çlðq]ÈÉ@ìDãpNɉÀìöSL2'hB€@_fƒeƒäHb* Ie—E†›jÉÂ÷@\|øûl…“؉>m”ZÝ+ÙíïÕ²Ù®k®,Ÿ“»VŸ¬Ý qipÎ×…œ ÄNÔ8甜LÈn?Å$sý$ Þá]JˆKƒïÒNåd v¢†£¤OìöOÄÐÙÌ“H|2àQª‹Kƒs6ø¸>æd v¢Æq8§äD`Š@vû)&™“ìG ið~ ïRB\|—æpÒ('±5œ%Õx"d·"†ÎfžŒ@Òà“R]\œ³ÁÇõ1'±5ŽÃ9%'S²ÛO1ÉœD`?Iƒ÷cx—âÒà»4‡“F9ˆ¨áÄ(©ÆA »ý1t6ód’Ÿ x”êâÒàœ >®9ˆ¨qÎ)9˜"Ý~ŠIæ$ûH¼û”—ߥ9œ4ÊÉ@ìD 'FI5žÙ퟈¡³™'#4ødÀ£T—çlðq}ÌÉ@ìDãp¾\2kÈ´ñÁ&|Kþm@€ÁšO\a— çv?åè.J?ùúo~‘_Ùú¦»£ZàѦô/?i°]¢!×õ%õî¬4¨Ú;[}ÚéNø§5NƒýüŠàZóÍ¿:bƒÕäêùí[ã n–~òÿôïØü¿ÿéõ$®´L霤Áþmt‰†A¯ÔÎAö¸ã„:Qã8œ/—œ4ørxVà„ÑáPœcDí]'˜²Öå6‘4Ø­i®U,èÕTíkmm¯Ý ÿt¢†·p%“‡3Ù™ Ÿàf¥Á9\{Ë ¦¬u¹M$ vkšk zu vÞé×sœðO'j‡óå’“_nÏ œ0:JƒsŒ¨½ëSÖºÜ&’»5͵н:‚ª}­­íµ;áŸNÔ°ã®dÒàp&;SáÜì¡48gƒko9Á”µ.·‰¤ÁnMs­bA¯ÔÎ;ýãzŽþéDãp¾\rÒàËMàYF‡Cip޵w`ÊZ—ÛDÒ`·¦¹V± WGPµ¯µµ½v'üÓ‰vÜ•ŒHƒÿâ÷•-Ô>ÁÍJƒí³Áwß©N0¥ÿž4Ø¿.Ñ0èÕÚy§\‡qÂ?¨qΗKŽHƒeMÝË¡{ œ0:JƒícÄÝwªLéÿŠHìßF—hôêªö%&ÞP©þéD F9% .3{ïØ`,p§ÏÞ¾)9ùw'¸ÙCiðêlðÓéT'˜ò°nØMpÒànPÞ—  WjÛïôïËbg´Æ ÿt¢Æˆ_TG ü7ß|öìùÏþÔÏñû˜xñê“O’ØuN¥Á«cÄÓéT'˜òÀŽØItÒàN@Þ›˜ WGPµ£ô'üÓ‰Q¬¶AÏ@4Jó7?þŒí‹¿ô¥ÿð•¯Kz•çlÀ$O©œàf¥Á«³ÁO§S`ÊÚmÜ&’»5͵н:P;GÀãzŽþéDãp¾\r \±z Ѝû™8 F‡Ci°}Œ¸ûNu‚)ë†Ý' îå} zuU;JßqÂ?¨ÅjôŒHƒûÏþðüÏ ÍSZ8ÁÍJƒWgƒ+ wß©N0eÓm"i°[Ó\«XЫµíwú×"±v'üÓ‰-hÔ9" 66-‹íGà„ÑáPœcDí'˜²Öå6‘4Ø­i®U,èÕTíkmm¯Ý ÿt¢†·p%“‡3Ù™ Ÿàf¥ÁöÙà3Q½¤®LyI»š*MÜ×Ó)ôê@í¼Ó?®—:áŸNÔ8çË%' ¾Üž8at8”çQ{× ¦¬u¹M$ vkšk zuUûZ[ÛkwÂ?¨aÇ-\ɤÁáLv¦Â'¸ÙCipÎ×Þr‚)k]nIƒÝšæZÅ‚^¨wúÇõ'üÓ‰Çá|¹ä¤Á—›À³'Œ‡Òà#jï:Á”µ.·‰¤ÁnMs­bA¯Ž j_kk{íNø§5츅+™48œÉÎTø7{( ÎÙàÚ[N0e­Ëm"i°[Ó\«XЫµóNÿ¸žã„:Qã8œ/—œ4ørxVà„ÑáPœcDí]'˜²Öå6‘4Ø­i®U,èÕTíkmm¯Ý ÿt¢†·p%=ÐàÑ”ÝhwRÊXŠÕs[Ë×û&œ¨aoÔ nöPlì$£b£];\ó%ß¾™Ï?7÷SžÛ -µ% Þ‚Ú8'èÕÚy§\÷tÂ?¨qΗKö@ƒ¿öoŸýã÷þø3ØÂÛ7$ØÕ`y÷ð·ÿù_ÿÎO?c#±ÎXZË¿¯É?þÛ¯¾ßÛý«¦Š‰–Ýsž0:Jƒ-më7“üÓþ;݃îºÚÿÍ?{öwú˫Ŧ†îÛO0å´ Þr’{³ˆ}‚^AÕvbôU5œðO'j¬Â·€üìÙs¨ÛÏþÔÏ={ñŠ]ϯÿæ)óÏó  v•Âj*ªt…¾§k¢×;:Ú¤ÆèÜÕ]!Z³ÏÞ=¬þ‹ßÿÕï~ü_J1 ¿}SÛ;{â nöP¬·®6¹©"ó_~Æ)ÒWÁ³Ê™M”’·^5Ðq=ðSÎ6ßUfÒ`Wæð£LЫµwú~ ¤‰þéD@†kUÕ þè£/ü‹ÿø_ÿìÿýÇ¿ùñgüÂ.–ZQüÑÇ)ø¤2» Õ1–‡@Ê´Ò¾ÿ­Çyiì.iÒ”oT£Iæ°0z %ƒÿɯýÌÒÜcq˜ï¾ò‹¿@1ˆ¿R~(j”>at8”Ç{x/hÓ÷è®_úþZï”ÿ?þ¯?¤p)ùâ€Ï>Â8º¢Æ ¦u‡»IƒŃJA¯Ž j{°¸E'üӉĂ–¹œ Eü‡OXüî§ß®éQâG?þ ¬ã'?úÉ—Ýá¹ÛÊñ—¾„X:[!“/^1)-™#Ûv[Õn­åÙË×Ü>°ýÖ›¡3Û,&ÌH—ùögÏ!f?ÿ{ߦ°”G½¥Op³‡Ò`å©6¹©ŽîhV{`©å}È {É:G÷@´8Á”R·‰¤ÁnMs­bA¯Ô6Þé_ oÐÚðO'j5¢EíËi° fؘƒß2+ˌْæ\ò ½aÚ“]ÅËË,4…IÔ­ÌKw¢ÁF5–Z½ž/±oß”‡õ/^AngO¡E´Æ?üÁwtO¥ÁJߨ5õ@î/@¬ÞˆÉ#½ú3•hN”™áZuMÝ©èSÖæ¸M$ vkšk zuUûZ[ÛkwÂ?¨aÇ-\ÉËi0ˆýöŸ=”iØ—¯ÿå¿úwl:ùä(›92Ð`ÀdÐ'"‚E¨”PUÁœb¼gÄFÂb{y–P(¯ÈógW£µ~ˆ–Ü”ß=èS CfXÓ¼7÷ñÇ>­÷7{( Ö¡µ×Ò)Ãí¯¼öKЯaiáÉU¸®Òq=ðSŽðt¸›4Ø¡Q<¨ôê@íêÃ=Àxg:8áŸNÔ¸3ã›ã9 ù„È<k 5¥‹½|ÍÓ|ʳ;*0Úµ–¿­„•ns[Äx$jÏ®UMu<B¼{ •Çô:€Ü>”¥Õ>ü#å.i°qŒhê ÇÍËV5Ûê}“UøÁ=0è@ÿa?Ý»—4x/‚wz~Ы#¨ÚQ:‘þéD(VÛ çå4˜É1Ø,éa< —5Ðê+HÓQŒx`y~M¢ðõ©´±<³²ŠÊÈ2œHèìTm9F5¶ ‡cCÆàðÄ“”…¹ïpMrex™®|ä6Ÿ)«à~ãÍ7§…Op³—Ï·ö@¹£‘¥60²m~t<Á”Óþã-'i°7‹8Ñ'èÕÚŠóq‚m\5œðO'jÄµãªæ—Ó`/ÄL^À犖µ –â"ä¥þzá“Ð)Ÿ½<ªlPÂe%]^›R%^Ŷ°«QOiJ '÷ü!¼k @ÄR #” Ç&黤Áµ«(x6õ@ä€X}1“[0ðœ]žNj´ ?´¢LÐ^1܆CIƒ7€öN zuU;JrÂ?¨Åjô¼œ Edf:Aô,ïà³¢×RC¤ð§ß{\‚g)”¯µ<4˜RjcáQKÚÖüV5ꉖÄhÁ™Á&ÂA9‘ùv9Z–‰¸QbÚ(³ëÓ³Np³—Ï‹uŒ=pÔ+F»S›„súA=É'˜rÚ|o9Iƒ½Yĉ>A¯Ôî2B9±‚75œðO'jx³NG}.§Á´…OÈ13ÉVÂ^¼ú¿ÿÏÅuƒ¥0 -l$˜ÉÔÑ@¸½®ž`J½/y8š4؃êôꪶÃ0«’þéDYˆî#Ó †«Èa~ËG+–?¦ æÌ[ÊC|Š‘Ðƒ[Ë#­ „ÔgßûmݪvSDëåkÖï"à>¬œÎQ6 t9ÁÍJƒ™W ¨‡šz Ðq£Á›†üÒ ?_££Šû0Ñ$ü¸x‚)?l·Ç½¤Á­â@§ Wjïô`O'üÓ‰ñìgÖØ f^·Ò¼‚¤«_y…Å pX¾V¤TA9Ë"_‘3:4Tcthç®HOº†=ÌŠ…CïGMÃDeOËŸ0:JƒGÍœ6Pršz ¼—Û È0wܸµþYN9¢ž`ÊV(Î/Ÿ4ø|ÌCÔôêªvˆ.’Nø§5¢Xmƒžh0ÄŒ‡Ë²pyʬNfÛ[€À s¦ižþ—GØ·ÐH°;-Ssˆ”a#Án=Ô1aQ{OuìD°ìíçGÞ=°x‚ù].˜f´à†(ù¬³ñ9ž£ÔdUáUÓSà ˜#&ÆI<꡹+½:PÛx§Ÿ†Þ€€þéD F9Å –¹5™dÓi0•÷’^º‡^˜b`ãùµëåyAÇÜå%~ñŠ]ÝŽ–á¦nUQCɤáÃCòÒÖ0g”FƒÙHKy²[ù.i°qŒÀâöÈ7lÂW—zïo‚í| -ewëAú!8ûÓIƒ÷cx—‚^AÕŽÒ…œðO'jD±Ú=/§ÁB þþ;Y?ZQ?õ;l“–Ø ¬£|°à6»Kb‰„Ôs?úè „Îr¼x,Â)ÉÛúpÂ¥¬,˜öîA˜$d’/)°1+(ÖªIlTÛ&…§“á²âÜP‡QšF•íåëEü╬Ëq¯ŸÏ°Ü>{`…ñÀ[hº`X óÍ¿¢díÕ–{D¬*åˆ IƒkÈÄ Wjïô‡Í´'üÓ‰FÐ"»œsÃd¶¶~·bŠ$lbô‡2[{[SB ¦±ü'¼žS‹|÷Óo×ô(Qgê¨K¦F><·–‡î–IÔ÷D.Ä<¶Ì©B†‡ÔkƒÚváB´Juï8 ZNíE1î ˜ùçÉZÁR˜E’g_õ:at¸<6ØØ«õAŒ+"±Ã¥%ØJ/þ*Ö9¢VµO0e­Ëm"i°[Ó\«XЫ#¨Ú×ÚÚ^»þéD ;náJ^Nƒ…©BØ­• [HÚF9ñbµ°B*"«¶²ô,‘hÙçÔµ.¡Cò96f¤eQâ!§­%YDB^"$ùð"&Q~øŠ j7¿Í7²œpr™à•¯ÈUU§ û䓇GצÓOp³‡ÒàYó01ö@9K Ó¯d—Ù~À ”ÝQ_•+Iœ-Oæ=°Öu‚)k]nIƒÝšæZÅ‚^¨=ƒ®ÅðþjwÂ?¨qö­-ºœ S%Þ’çû²•˜ÛÝC§Áˆ)ƒcùåkžû³éå…CR†‰V¾M̤߬"ºçÑ"PW[?}xT–, ò!‡êŒâ¬ædÒ:î ?|û ë^*Iþ £Ã¡4x×R3›z`-,Á«sï£JuÓSø¸x‚)Gu¸›4Ø¡Q<¨ôꪶ‹[tpÂ?¨aA,h™Ëi0¸1ôË‚·ÐN6©q†“0+;Ì™MÃ…ä63r³SÍŸŸõö )  ­ê¬éçz¤Œjo« áh®œ ïay?Ž»Yfm©ü nöPl™ ¦íM=ÂC‚Þ²Ò£>D–®øÁJ-{‡õÀL9m·œ¤ÁÞ,âDŸ Wj[îô€N 'üÓ‰áÌgWØ F[ m%º•„Î(3ä6ìêeâáô²î4˜ÜC “ÒœBbéy÷|¼µ·üתöX’*|\XÝ&ÆíC$gO:at8”ëæ6ÙÞ9 ±UrM ¥ Ó̼ Ô² MÓ»zàd}¶¡üL9¬Îg:i°O»\®UЫ#¨Ú—›Û¨€þéD #h‹y Á0Uæ'Ëv[¬Lÿ|Åê"üÆî"ì°‚wÄ6@´ä=ýò«þ=êp[-M”QŠÃ–+$AçÌ jߪl^ˆÖÛ7Ä©ÂÞ%ZQ›C¯«¥ ï¦gàf¥ÁÕ@Ó¦ sšz ˆUÐ,ò1=}C–vÀV=J[z ‹‹øGè»ÞO0å‡yÜKìÑ*t zu öêm¸t£ªà„:Q#ª z_Nƒ¹Š¾y¾Ì¢ ÌO–P^e™2YøÅ+âØxMá¥V²îoè^ñì¹¼ªOE°Ä¥òPJJlD PÅla!?†y’f3-SfS{›pšV{[Fƒ´™U '˜€9!(záF‡Ci°eŒhíô+6`”ð`ý® ðZ8d“»dò-=°®Bùº­öÀL9Ûß\e& ve?ʽ:‚ªíÇîº&Nø§5t¬B½œ×Wºd™28 L€ÌYT…'ðF˜¥°òá`y7ÆB4¬¤uŽ-o?±ðôF¶YÎÌ Žò• 2ËGµgu&Ó®öáȇЂm„ºÕŒ&¨·¤ ª‚ƒ Mym$œàf¥Á ¿„CÍ·÷@ïcLõíÉ…Ü‹ ^ ÷Jµ.écKäÜØ…KÇ«¿z<Ç”µ™nIƒÝšæZÅNptG4µ—œÉÕ=5™Nø§5îØúNh0sYðUVëz ˜Å\ qÂ=Þ=LŸN„eÑFÉÔ…ãOàÌ×É«Oü2¹:’Æ.ùðäp–Oö´°äØÕÞ ¼T1 å™;/¾-i"pTh°ÅN¥Á–1B¬céX§ôŠ—¯¿úµ¯‹äUÁžŒðb‘ÛWQë{ ×éåšÐ{à9¦,môý—4Ø·}.ÓîGwDÛ‚ª}GÈtÂ?¨qÂNd^NƒÁGó²ø-+î2«ð) ¯°ÌÁ ÂLW\„Ï1[))t¨|Ïîö‰:~—¾Œ 凿rî0g”–0ìjO_{ç /yÉKŸ|õö !ÓÜt”˜ØeÎŒ'¸ÙCi°>O[AnêëàrùtWŒ‚éÅ4tõZï(qh<Á”£æ8ÜMìÐ(T zu ¶åNßÂupÂ?¨Ñ‚F=Ð`fVKÌä‹WÄåBHf?¦\›Ca‚"d¬LÍ­‘ʳÉ”˜Sª¨Q"jÔ@bx˨ÌÒnyœ­òÉVµ‡­ ‡h•íöU;fªuLÐSøÏÖYH™YÄǺæÞž2ÖR[‚)k]nIƒÝšæZÅ‚^¨Ý:V^‹s¬ÚðO'jIJ]“¶h0¯Ïó2ª¼yD‚0ƒ¦&PxÊ'E‚°&Êd}0X+T¡ÕoŒ„³¢Zy›ìí›>úBYüê6Çãi¨Ô~µ7-!ä;ÛXrÂèp( 6â`ï­hÑyÉŽçH&Q7yÒQ 7Mœ`Êi¥Þr’{³ˆ}‚^AÕvbôU5œðO'j¬Â·€ z•®ÔD¤#¦:<·îÞÖcQbÈ0 ² ZÒ#áeÊúÅ«úåb™‹“ðc‹´a™‘dm.B)ªO$jm£q.”z§šˆž'¸ÙCi°Úñjbh©šVЮeF‰î=p$_vq•9<‹Ì03iðLWNptµ®Ž ÔÖýUǺž ('üÓ‰wÜœÐà+A¶x Öc5]ÖƒðÛZ×H8SÖu ®&Ê\Üòz°K5Ž$Sl›p–®­TŸ4Ê áö·ö »›[÷ø„ÑáP|Ä1‹ö’‰ÉŸíðsûšÓ~2­ŽZôÇ('˜rª•·œ¤ÁÞ,âDŸ WGPµ}U 'üÓ‰«pÅ-žß>ú LcÖ ÷§¿\§I°;[L2™S-ï}ø§¬kÉ3kzd„èÂÑê Û§i©ti‚·ª$‰ºÊÖl'¸ÙCip5ý¨Õ{v+Ú$Ø*€M2—¬ÓÔOšúö ¦lBà’ÂIƒ/Ý¥A¯Ô®ß?Èá4tÂ?¨Î|v…ÃÑ`XGå6jÎ~ôaØ|¦IYz‚ƒÙH,>8~÷@”/q°²²ð-âw(g{ú8ɪNKDëÀw,8Zä&ü„ÑáP|ÄÑà’‰f¬ÓÞO¬}û,S.5ÖO~Ò`?¶p¥É ŽîˆöUû(Žé„:Qã„ÈŒBƒ¡¾Â~úáo²+¯ƒéHRžp‚"Øxÿn‰õ±N…¼kO ÷…`³4–.Ürô8Ézí3Dëv ä;²Èðmµ%@Dø nöP\ï˜t¬šŽ6¸$yj ýM,}û4S.5ÖO~Ò`?¶p¥É Žîˆö¢öwúG¨Q¦þéDˆ4ê…óæÌM‚$I°¼0[árê"Q° H¬,€à1Xb‰ò…Šù)iVàÜý<ê8ɳ ©™K¥…;½| (F Ä "ç„ÑáP|ÄÑ`5Ç(1µNk?±÷íÓL9j£ÃݤÁâA¥ÝÍ ªöP!Ó ÿt¢Æ;‘…CJ™Î…¶[7aª ’²¼á¾p†Bhß=¡”‡3PJ!*½xÔq’—š3o”’Ü;Ôû0Ñ9ÁÍJƒ‹Ñ{ÿ5¸Tù’uìý¤µoŸ`Ê¥ÆúÉOìÇ®4 zu v¯Ê•9œ(ã„:QÉQŽP# žm{]Äuö¨d²zÓwÆÙc¦‚¿úµ¯Ë‰åcaêT³RéôÐq’§uIÎt¾‘|AŒèV æÌzO¥ÁÝLjV—¬³Dƒ›úISß>Á”Kõ“Ÿ4Ø-\iôꪶ+Ó+Ê8áŸNÔP€Š~(< ž[ækh&eæË ®ê²ff“? 凢ö¤“¼¤U!Z“ÆB K€´D¿|­K‹ØÜì¡4¸ûlp+€KÖ™½I¡pS?¡°½oŸ`Ê¥ÆúÉOìÇ®4 zu v÷;}Wv¹V'üÓ‰×ÚâÐÚ¯¥Á\ÂÙššYOiuD`j·åפ’Eø •â¥þéßlf-f”LùY9³™UøB¢? Ã$ U8[¦–?at8”·vÚp%Ñ`•S{¬äp“Â=H=úyÂÖOfú$ß^Qûö ¦ü¼^SIƒ½Zæb½‚^AÕ¾ØØæêðO'j˜a‹WðZÌ*©®ÀJe»Ù!~x”pVÎâÛ,€6OkéÏ>#¶ZZ’°O $B7>îVsä{ÇEIYTíö˧8·–™&,’9k›p©Ž8¢ÐDðsÝÞ¾¡<Ó’ŸçLR'¸Y>a ‡¹¿óŸþ}Ó†d 7ZÏ rô.6ýDýèE K¥Ó%ÏRtK?á›à ØáFoÔ¿–r‚)G w¸›4Ø¡Q<¨ôê@íY7âÒ;ÐÁ ÿt¢Æt© ×Ò`jgNŒp…º^±—úh^Bv_¼âƒYá zóÑG_€JÁ÷dÓi3Œ‹ ™•L2õÇòbüç@¾C‡ÿò_ý;ÖUƒ´ÔÕÕô [‹dªØ&œFqG€2%ÚáÙsË:o°ýRþÆ0RwÂè`¡Á‚žh‹uŒdÉ«c„Üwn¤ËK—7 é‡Õâ£Äç7V·µ—ôäD9A¦,ÙG‚Ý‘Àá®´Tïå‹ÛïÃÝå~òbÍ¡¨QúSŽjt¸›4Ø¡Q<¨ôꪶ‹[tpÂ?¨aA,h™Ëi0¯Æjú~fzƯ€ÉÑ?úø/¥ä™Ý¥ùL‚{ ¥yË*a¤! šð—¯áä1 ±„9Sž*†4f7æÍ))ò²ÒNÉœ¾M¸ÜÈhåî@]çv1=ÎÝ%‰ˆ °BùNp³: †ñrÃMG±ãí&E~a}˜FçÖÙ`„ÓO0($ “Erûö v§vÞ.ä—­<’XþC zì¢ðÏ>£—®ö@ìEÙd±š€õÉYVä³L©ÔîäPÒ`'†ð¦FЫµWïô½AH'üÓ‰ תêå4’ ªõ—œùV¼}Ã%ÏQ Kù|a–³å)ÌiañI—¹ÖgÏ—Ê#„b" X( õbÚ™ßo¼ùæ´ |òÉc0•Ý%BÞ*™òváB´êtzy3n™ÙJašC¸H’¨­ž¶ñ„Ña•W®Ë<*ì½Pâ®G§ Ël0Òä‡N´¿ €| :Eè¥åö‡Žíªô¨‘9äÍ8å¾£ÚÂÒyfQnÓ^¼*š¿}£°kÚr‚)§ˆyËIìÍ"Nô zuUÛ‰ÑWÕpÂ?¨± WÜ—Óà/þÒ—dBU~e‚wŠg š…É /ÁИiáa4˜6JÎ*ëƒT0S*3Àðy} Å*9J†Õ¨cìΓSš$sŠ]8œŠÂ•Ëtú’&µ0§ 3™à?lÔ(}‚›µÓ`!ºbô—mÊ~kŽe6˜î@žGÈ­˜€³Ì–þ@g±Ú£äFl mÀäz2]/µo?)¼|JÐr™™o¸{‚)‡ÕùL' öi—˵ zu ¶â|.G5ºNø§5¢[SÑÿZ QÓ>FíÞ´„§Á?§ K)sq· ™:.饩ã÷"xô_I3›mxdæ¿0+Â8¡«~ oTq"¦•Sš$#Ç.\ˆM“°g袉LNÊrÁ”D+~µO,4ºN¬(,HòËa •ôNH^# Áô"˜mñÅ+ª`›í%ÄV\ÂQÞ÷¨¥ÉXQ†ÒŠu¤eŒ=’r§†òœÂnUrš8Á”ÓJ½å$ öf'ú½:‚ªíÄè«j8áŸNÔX…+nki0s¿ ßx/=Ôt´•0ÎÉÒ¸#[ ‰ܲð@yT’](ðŠé¡i´§UBŽ_¾FÿaEÓÂM’9½MømjT¸xí üñŸ2(Ì3ý2÷þžÅÍžr‚›Õi0øö{{9‘'Ðþ)ïå y‰šÖÆ‚•t*rH&º) ŸC‘@ו4•Yå¹?@F,9$¡ oê'H®ÒÊ<ÿÕ¦œk½¯¼¤Á¾ìáF›ÝmEíêŽÿÄe:áŸNÔ¸ãÎp- ®ÀBJY«Šm–ÖbÓ„Nù¦å%GB4§G™ÚeNJ3ÜЪF#O)´ç®<ÔÂ<(“t“dN± Ç B®`ïÅÞ”ŸÖþAÎû•ii#‹i@2?8úáÎ £ƒNƒ™ïe’“›™û…¸®²‹dãA¬±6}qo(³¦%Údé{÷ní'ë2ÃÌš>Á”µ.·‰¤ÁnMs­bA¯Ž j_kk{íNø§5츅+éKÀ¤°~Ù=F‰ ˜ÖBíiðü½®CE‚ÌáJõ,òáDtH4‚ä×W®j1I4Iæ”&á”—E-ЇMgt€«C·j˜ÁHÕáî nV§ÁÌúÖ¹_n ¤ò‹þ æe6¸ØîÝd›ÂÜÔM†C ¡ 2—Pen0ùˆi…ò- Y"-P ú>‘]$,MðJ¥ˆ¢… s^ ÁM’©´U8· è° Xq´¬óöî n"tÊw‚›5Òàï³çX_XkyµíF‰§ì·æ yµoH`0Ò0¢¤WoÄf;ÕRjíÞ­ýdV™ÙÌL9[¯«Ì¤Á®ÌáG™ Wj¯Þéû9œ&Nø§5™Ϯ°L<-T,Q»Š¥&ç„«Èט°Ýà–bƒaVõ»É,ÑÆ.jÀH‡Ñ¿U±Æùö L’9I03±£°¼ U‹I¢I2§Ø…×¶ Ö’ŒÂ¤}m©F# ®¤—»”‡ëØŸ=¯¤wš@²±oP’ áÑdï«ôÀ‘ee·ZatTh°½{·ö“QuÊî ¦Tjwr(i°CxS#èÕTmoÖ_ÒÇ ÿt¢ÆJw9 Cžã 1cU´á T³ðBl˜µzCb– Îž8Ì\š»M˜&eC%=`Æ"dRðPþ4Ý$™ÓíÂçß¾!À•ûˆ‰ª® í耱Î~Ou&ç7ÛDƒ%<& é"6å¥9$¯ÎK«¹·ª4¸ôÀµåG¦X-õ(J6uïÖ~2Õd)çS.Uí'?i°[¸Ò$èÕÚÆ;}WhGQÆ ÿt¢F«mÐÓ †ÊJ,ë¡:1ÛÐÆé)Kswe²”ÐàWL“¾x5;¯[¥QŒeÇŒ<¼I2U4 GçBÈoSÙPD¦Ê«’£„4ªL03RÞ’•îž0:iðã’7êKceWøðtXrl#°Nµ#”˜ûš!–tv<#ÙÞ½[û‰E7)s‚)íÊ\U2iðUÈ;¯7èÕTm硪ç„:Q£Âr 4˜);èŠðƼ=¶`îni¦5$FTôQj!þA9:=d—̹MÂQBÎ[oðvjQ4ÞBÉÞ¯u ßÓ§ŽOp³F ³…ô–Û“÷Î ½D€+ ÞЗ€ˆmj>=‡S–LV»·H^UÉÔOXàbú7›ù¾Ø ¦|_•ßÿIƒýÚæRÍ‚^¨½ÁY]Št¤ÊðO'jD²\£®: æ*ã14‰‰˜mÝìsqeÜ÷#8/»ýþPžÉÏ©¼YïE!(ØH¬2–©Ø¥œã$SãPO¦%ùHÜ’äÓpˆѪLû1‹C=ý„ÑÁNƒ+¹ÕÙo=jï,ñQ1ÀÕ¨ƒVÑ“ÒÀY–b#¡ ·ôš0^.™ºÂÛÛ7H.™Ë'˜r¹r/G’{±„3=‚^AÕvfüEuœðO'j,Âÿ€Nƒ9Êä#,ÏЛø°„ n`#Án\!l¬-Àt(Ÿl•íÌVÁ*µ2ßXæK_¼bw¶Ø†Ìã$£ ¦‘Æ ‡×x2¥›ÞÆܬ‘ÓFúÆt«¤wš@²nîjGpàÈB,ß=ÔW#ëÑQâó«]­á ÍVÖ¸X{'ÑÒO ÁÈan"VT4Ru¸{‚)‡ÕùL' öi—˵ zu vÓ-ùå8ÇRÀ ÿt¢F,Û5i«Ó`®²GZx£L2  æm,}~¸ßqáf…ßÒMMX*Lç£òȼ…°KKåá34GÛ²f vWIÎ’´aþq’¥ô,Û‹W<AbXû( ‹+°¼xUWifþsT¦îž0:i°¬“öØÒ÷½‘Ý)û­9önÂ'á–%ôbO–çö­ Ë­ .‡ ×l‚1"ÞéNÄ®”Nµl{?a>ŸÂtT4—ªXR[´:Á”³Íw•™4Ø•9ü(ôꪶ»ëš8áŸNÔб }T§Á]Voà•«B)o¤•‡°à€£KdØNBx@ÿÝO¿-’‰Ú%ÝRH Tá«_ûº¤ËkËkaµ.ñjWò8É¢‚A Oã³eËŽˆ…9õ–Úr‚›5Ò`Y/m…©ÖßJz§ $ÓÞ¥¦ óËwO^¼‚O t’áÑšfªVظÜn<~fe¹Gq¢>DX±Nk?²ôíOÔ¾öަå6‘4Ø­i®UìGwDQ[|þÂS¦þéD;î«4¸²\/ó‡PJߺգӄ3¥Æµ€i3[Ëò‡ÓFøé÷þÍ—üùå‹l²ò˜^¸IÃã$£†-nD%¾¯˜Kêˆ7 9at0Ò`n»PÓLéîRޱ €|-E@ÃçPÑ,€2£Ëṵ́))°  ,¤Æ €èm<ÁÍi°Øe‰ñÎæ#™N5µÅ(Gn *Ÿ„‹2Í;*3ÜÞX•µGqbù,Ý-,G¸*šú‰ÆîlØG6C=GéL9ªÑánÒ`‡Fñ RЫµñW¼KœðO'jÜ¥‰¥Q: †À”˜¡r%T„1îĬDD@†í4˜y`èË_y¶»Æ+šÌªŽë¤ß¬¡@²ò…Qf¶Ø†Ìã$£L¡C·À`.ðT4,ñ-ï©>if8•Â'ŒM4˜~ÈÂá6K€%ÓØ1 ÕzÇBÄ‘zc€«4¿-÷qÃbÃ4UHÈ =J·NS?¡p1ýûåïØV:JŸ`ÊQw“;4Š•‚^AÕö`q‹Nø§5,ˆ-£Ó`Ž>R_ˆÓ{ö ¨S¾JÂHBÀ ²M(Bpi4'zHá"°¼÷~5¶Z± <ή»›<ãFsq<òÞ,Är"Ë\èņàLQžËQ¨/ž5–xxt”>ÁÍip þ¼CÞȼNƒW»Ç¨½»,D¶ü7„·¦ev}ö$c÷ž=WÏ”¾õ.’òK}ûSêÚz8š4؃êôê@íê‚¢]%'üӉѭ©è¿NƒßO32÷Ë#`6æ‡ùepW8°u6ø¶ä.TȪ¬ƒº .5Rïð Ý£0õÖÄ"4rȈx<ÍJêyëѧP²Ûª $Ø]?Ç\âQáwìò—(ÂMD½6Ó\ÛbÁF# †îÒO¦›Nƒcİ{#A‡YDä³Ïfïzæ{TK÷Vj\:ÔÔ·O0å’ž~ò“û±…+M‚^AÕvezE'üÓ‰ PÑé4X^‹ºûATð[2¦(LØ2L˜e]‹ dÄu+¤<Ħ'›¼ T Ì&  å廯„ C¶…UÎÒ`fw…ä Ÿ„2KÊÇ”™‹ãÙ4»ÃI¹‘2vÉr"j82+Í î ümÑ™ûEíáY¤ÑJ/|‚›µÓ`/­«›B€å’1Ó¨ÉÃ]éEä€6·²»Ú©„|²`ZŽ·w¤Éäb 'ÝÔ½åܦ~B¥ö¾}‚)GÍw¸›4Ø¡Q<¨ôê@mã¾Ãéà„:Q#œùì ë4XX.€e1æ~¾Ý^dƒ§í¤Á²ä)lKI—÷‰VëEUhá0.´°DušÀQ œ*(üH,Ÿ=Ÿ¥ÁòäúD»¤±KÙT)Âå9; ‡ÙZ‰sP$0ßXï#ø¸*……h)†‡Np³F,1ä•“(w7ûfƒ‡-­éBƒÕ%û¤çc”Úië¹³ c÷ž=WÏlêÛ'˜R×ÖÃѤÁ¬àP‡ Wjo¸Ówˆ¿O•œðO'jø´Q­Œ4–%£?¿LûH@·–""Èß6ÇE­_×£ÇÜ£Ý)&ÓˆÍRæÝÃê‰SQ³9Èé%j(_ȪLMȈÿK’­ÕIìѹúî £ƒ…3å+ÑéP_¦aÙ%Ž—È}ÙÒö8² Þ©—;µQ¦î-Kða©Ñ‰:þúQ½WŒÎ=Á”£î& vh*½:‚ªíÁâœðO'jX ZÆHƒ¡µL{òÜŸ‰2æaÅp`fw¼ #,ˆ—¼di5ìê§@œjy9  }"šW?qõ(ÜIVf€’•(ÍÉêÄ«LnÍJ¬ïXÈ­|/®u‚›Õi0ŒÚ©l#ê;ÜEòˆßޱz¿ßÔ©>xq[ÑŒ†³/]–Þ=H|‹$H¿¯öÔÿ'˜òÔölª,ið&Øîÿ¤ Wj÷rõ÷oãö:áŸNÔhÇ/Ìv¡ïQV¾¾¶ÄßÔ ]rN0e=’4øPxã zu vÎ×ëœðO'j‡óå’4XfP €Ð€ÛïÉ4XÈç~÷Óoë¢3ÜCÊ”eÐn$"=3¬Ë•õ*Ëš*6(Ûœi² Ѥ²Àï’»“ÂÃFX“Êhc¯ùÆFs#ËùÖ„ìÖß%L¾ñFlj;½SÁ™¹ŸsˆÀœ»Ú͆ö–õ먶ÜÝ:ÊVÓ'˜rU‡Ë $ ¾Ü>zuUÛg˜jå„:QcŠÏÝäi°¼{drr¸Lƒ…T°¨á¾Ì³ÁyÖ¬ÛNX?KW»Ñ`8̫ԥգœŽ(JŽHîE>…cËD"ñÌLöÊ”c­}˜ÂäPâGyÛÙº*ÿ7»Jƒi Ÿ'$¦›NƒáŸµ-K‰ÖN…ÜO‰4¢VØ]’L¾ôz WL^¢ˆ{õ¥Þé¡L9­Ô[NÒ`oq¢OЫµ{¹z'†p¥†þéD W¦é«Œ…3å%$„„Â{G‡Œsq­Í)ý xùj$sÔºH;D…¥wy‹ ÎÌ›}zyûÑ*YÖõe×~®^Rˆ™ˆ-¡§·Ø`å @•e5Ês·¢n:tÂè Ó`"`uc&–ôðW§ÁÆ1¢©S6´¶ì·oV{T¡ÁD¿|ýøÅ™¯:ö@o¦lÒç’ÂIƒ/Ý¥'8º#@ªöP!Ó ÿt¢Æ;‘i¡Áð[†~68 Óž ÖßõîDƒÁ j! OI¸¯Ž$… _½Q‚9—xQa5íK=@Ÿx>ÎF¢»dÖ"FaÖ%(ÇË+ÙR/GчòœBc§×ß nV§Á Ë]=„dËl°`%=Š_6ÀT„Çr-Èûq ¿Š6’2 Þ·Ÿ(zŽ`ÊQw“;4Š•‚^¨½äO< ]'üӉѭ©èo¡ÁL˼+£?Ó_ÃmÈ{Géƒh0 \>Ü&²8"(äÕþ²rÚÂã#ªML™C–Åî¬ì ’‹œ·oDUð-90Ÿ.™°, U "jtè„ÑÁHƒyQ㇠… ·õÀÛ²fe5³w‹w4Pso ¼Ã0‚®uwC?1ö@ÑäS¶6ùüòIƒÏÇ¡°e ½¡<Ì™X 6 `è%9ð^H’¡|¤©kVø†L¦1áÕ–"ü7k§Áň“M§ÁÆfbñò­KøãÝÐòÓØr]-mxS¶Áõ”ãz Tq‚)k[Ü&’»5͵н:PÛ>r-ÂkwÂ?¨Ñ‚F4~Ëœ0d˜KOüyï(Ýó’‘|Q.DBÖ®¯¿Í¶½!N€×Çж„€.,k&!ü€â”Z§dg…JöâqlrÖl± ’‘S…KP„þ˜~X/ M™ôÉ´ŽÉU8v™ü|?í<RÓ'ŒFÌí˜ðTBâ„%<˜´NƒchPͤzYÙìÅ«rŸu»™ýÆŠ¼‡2ô+ÎLlc –[K7búIí$zkž`ÊÚmÜ&’»5͵н:‚ª}­­íµ;áŸN԰㮤C8a úB¢˜%HxD}‡»Ýi°„VB† Δ´BkÅÃUÐRs sj&(pRá%œX}1 æCú5[Ϊk„·Jæt^–#x÷‰‚8élŠÅt4ëfÈÒj(¦¬EÀQZÇ3}¸“´‘«l¤sÝ=ÁÍi0½mÒ;=„d Öf"¹.’ÆÇæ™ô™Z¦&ЄŽÇG1.Åf{T-4Ê  W ¶Y­Zû‰½Š&'˜²6Ùm"i°[Ó\«XЫµwú×´v'üÓ‰AhQÛBƒ™††É>üe.nÈ{Géî4XšƒXt–´,)¦P>¡Á?üÁw Ï¥3Ì¥¥/^Qx–’ÕºNp³à³Äc‡ùK/iËŒÒH.w(†?1_ Ãf·äÌÝMù$’„{@:?·NÓkaì(8‹AgËoè'Æ(Õ`ÊÙv¹ÊLìÊ~” zu ¶îÀý Q'üÓ‰-hÔÙBƒçŸ= -9Ÿ×gÇ4ú'×[Jù¸ûâ1І%> pZ¦‰s0:$S&ɆK„|ƒd™r„b1a kz¦4…WI{=f² Ç®™K‰F# †vÍåõÌšxñjD}‡»H6š±ÌÖŠ)ë/öýƒ÷ m掫€‹©QÖ³¢†™ú‰±J-'˜rØŸé¤Á>ír¹VA¯Ž j_nn£Nø§5Œ E,f¡Á ÷òØuƒe²·L‚ÝØܲNÿŽv²7y~ ɱðÊ¥©…9}ÃÀTjá­Z-Ñ ’ѳP÷÷k޽6åh×Ã}ÎÜÖN;ÁÍi°ð[È0P~É!1ä½£4’³ÁK¶›…‡ÂYMýJ:‘9pãÙòÆÌ ýd$YoÅ ¦éãp7i°C£xP)èÕÚuPë#¡l\)…7Fx¬?Ô†ùð{­ÚNÔ¨ téi®„€ðˆ¾wi8S‘T ¼—ôpwXx”>ˆ ƒ%F” *kä9#Ìmroï¦!Ó(œŒÇp≜iF«Ú87(QͲòCG_ÇlçPZY,bù°|°Ï; ®ãNº¥Ä Ä%1âÀìÔg›-þ›=Ô”ÙÔO(ÌÆ³:-›¾BÈ ¦ljé%…“_»ÿJƒ^ÝÕÖùÀhˆoÚ=ÓwìoÌÄv”¶Y”5п{—ÛŒIÇõnÏQa¿K¿Ê…pP·g2ðñÉ8“¥/^±»%Òbß`ã>” B¢hb”\% ð]yøøÛšY¥-%¸¸¾t”üú¼‘çŒ ÈçÖl–ëFQÞzh©GµÊiê'4Ÿ¨$9…Çô¥½'˜²µ±ç—O|>æ!j zu öpNc?Ô:P†ûÕCÿ?{o̪IϤiVyÍù3û; Úé°æzãͱƯ¡)cY£íuÚšXg¼¶–a–Z–ùš‚¡›6šÞ5Ê(†o/å}ŽJo¦ÊGÒ“:¯És”ÊÈP(’î E*;áÇ«¼8Ti`Òžb×Ñ6{"iÙΔEùxìN ãïaöÌò`"rµó N©¿T÷ø¾Jàg®WÒ*Û^üÑ /¯`’”[šösŽwéí?ØÒÄ0P2Ç«$pFVx›álp`óNüöDðò îåD¡éd “ÈÂà¶s„¡¨¬EôÙKUv"bÅíž¡~³œÉД¥¢ï“¿`ð}ÚâV’LÚ;š‹mãcº?½ÔÜÊ„~'Â47¹;è­Êìùš€¬Ížbö1˜A»¶r@qÆÔ_ÒpÖw·cν%æ”Ëç¡@é°Vy·^?g ¼ƒC†$¥ ùÈ™¾|Ç©A<Àæ099R¬s„ÓKJÃMµkx)kQµü«ìDÄ‚ß!}…(: )KEß'Áàû´Å­$™´w ¶Ñå/h¸ œb€”`Áà ÍqÃ[&í)¶&m³ç*ËͬºâsSTðî7µó]º‡ÙÓå™ëáÈÃr°¾qaÈ‚–ZæÀ`ô#õÚ’ÔrÞÁ!ùl/T3mz /þꀚ¶OóËŽÁ¼ê…>S²]z€Í;a°Ü¿„Fâ*F‚LN#Þ!á¸ÓO<ÍZT¼êLTÙ‰ˆ¥ítͳ¡QЀ¦4J¿É¥ƒoÒwcÒÞÑ\lì¦øªÓ‘£ñݬë#ÉÓÜäî Ûì1]0F\ƒÞHŽÐÉìö ›cå`À0{ ]Pc ´T1‡8n¿â3Íͪ8K<€«Ð>̯U3ÕLäøòÎìHÀ–Õo§”iz€Íc';øš=åùKíÎËqÚAŽSüÀr ïn„ó\Þ`4_e'¼íw¡qQEÚv»ô€¦Ü•xÃÓƒoØ(wiÒÞØºGvÊ·ñ€1ÝŸ^ê„vò¯ÓÞ˜´§Øj±Í+C…§2AÃø;™=½žXY<™Ä! ¿v³WK‘œ0Z8™§ã’ÄýÖ²%Vq†C@­ïhŸ4QÐY¶þ̸µWz ¨˜öMsvé6ï„ÁòK'ûßíMÉæ´“îT¤Óðäb>eï:fÖÙÉð@á‰Æ`@S«s·œƒïÖ"7‘gÒÞÑ\lÓý饑£ñMŒê‰b°bÎÑC€æ&×CÈZžN³gÕ•—¶v™¸dKößÑì~׿ |ñ¶¶¾|ÿ‹[6Ž÷R£è?$Áé‘&ÍùïÿüŸ á°÷ªâ–ZÎH¨osdák*é(ó.ÿüôçwûÞ6ï„Á8Æã¡=:tJoù Ûµ;ׇâý«´ÔîOu—i‹Te'¸¡§/0â±v°¼Á;UO >êdå ]=#6ƒLCÎNª×¶(náHÕKƒbZiÎ.í·XÉüþüÏþ‚Cσ;né逦L‹»gzÁà{¶ËÓ¥š´w ¶sŽpj؉Œy¿tiÁ`g4!cáèñ7iO±Uá4{žÀ§0í~ùš%›'¿“Ù Š¼Å-ØuK®FzÏ ‰°'aù¶M™À*l=ˆËÉ9-Å“Êòhà¡„Q©—úi½°y' –3–š å*X:žFè›&:YàNϧµ3ŒÝ鎛Nv¢¦¤59ÀÛtÏ,7ehJ£ô›\Z0ø& q71&íÍÅvâcÞ/]3ßÍ®>ž<ÍMî*ª5{â` @ë(Ù|? B h¯ ¢gOA…èå9;ŷϲʯ"FB´M‚Ó,OeVq6ød/á`Œ`Œ‚Ò­ ²ôþÌ6_ƒ_^‰dÐ ¨xói§¸w—†3ÚðWÖO [Z=sœÑÏN(Úß4¥_Ï¢\0øYš¿y¹“öÄŽ# ×âì.-ܤžÎdÒžbëÍoö„¿*–åW ƶÁ–`ãÍ÷ƒÁ”ðQø¥ß7L3ê¢v_^YýWƒ V«ˆƒ [l-KÒ©•ŸÓKìL‹T§dN‚Ë6 xJqÂàÄõœvxŸ! ›Ößì0<[™ý줪/\nJ³r“]\0x²%£¹Ø~;™¼H>‰æ´ ŽWÒÀWTÒ‹^Çß" ˆÛGëcùmr«ˆ·ïïŠ.¾§_ÉÙn¯ìU$ÑG1ðRþÁi–ìBæe›GÉ46äÀÌøÐCV' æQ‹ªqïN³ ¹‡RÔ¬ä{Ï[ÔV[ÕÔD[ÙI]_ø ÃWÖ¢ŒÌƒ åüž/]èž«´æb{ðÀq®÷ätŸ«ÿßaéÍMî:tš=S¿f<ÀX9œê;ËÇŽÐÃìkõ4Pø1²Ãúœ  F‡"x‰ˆÈn“[E¬VÓvU8‰Å‹$< ôç#“RЗ…lYßÏæ%nÙüÇm ¼÷4¿àR¨Ëä" NOK0¸¹7XH³J],ˆë*ÿõ³_t7e¹–Ó\Y0xš¦+褽±³Óßeå9ñÀqº?Íé.WsÝxY“ö»¾N³×†i >Y{XþôH©ï5>ö‚Nf¯eh•ˆK¿t¶‚êoQ»/¯ÚTJÁœGú*âx{øàóËWô gx)&®qŽ·Û ö¤t€–34F'¡}¯ó*6÷Âq´ˆÀ0xÛˆÁN àäFZ<"ÞÝỉ¶ÈãÁ'è¾ôå帷á3ß{?;qöµõ‡¾œfÉ ŽªX‰T“öŽæb;ñÀqx?Í©Ù=ýc_t㯊Øà“½TżŠ8[Ü ™ÍMî•vš=CÎ7‡b$ø%-<|ìf_£À#àöä[ÀÀ~³wC¦¤ÒßKœ‹à­"N‹ãFm˜F)Ùíª.sNK)¥Uúî*¨˜÷w™—O#pÅšX ß\Û&àÜÐŒ…cê@ß`ü©ï=gQ;…w²Øzú‚„ùÃ×Nϧ§ Ÿªè÷I0iï@l†MæÄÇéþ4Ç`ùÀ~–}IpjT°ŠØà“½TżŠ8[ÜIæÏïLg47±auÛüc‘Õá·!aî/žÎ˜“ö”}={î4{y;…„÷¿/¯„J{ßì+ÑÙYb4åì†ß\×~¹¿É*œÀ¼ƹݢpÜx·8:åy^IØÏÎGމ>±ù ®`Ý‚û”ËA‚X H+4‚` ×ûåX÷]UàI‡G ^Žã©'iûï&vò!‡/[óÇ« u²rÐÀ¤½£¹ØN^=5 v„ÁÁ­Z~h<•8å¼£·OÅÇ:ŒŸpuã-D2s b‚…0{6ÍÀìãA| ”s ƒU)ä<§0¸‡78êdå I{bÛ½¾¶qŸŽ¨NðI¾ïq*¿G©UÄ%&¥ü*æUÄ¥|ðŒöÌ8Ìàœà!Á% ‰,1“Ïoʇ¥ÛS@5iOI«yL?Ýì"9´$mˆ];÷Ö^cÛÏ\Ø$‡þ€:ÑG}ZÐ)çS;‚ðQ¿íÉNwYd«½…w7m>W¦ªÞÝÛêô¿¡ß—Wܰ€yޏo ƒ¤„;yƒ5€ø«;Д~<‹rÁàgiþæåNÚ;š‹}< VÀiËfUĵFXżЏV­äVÜõó;Á{ὕʣ"¤)•ÕÜäJÌwš=ÓkÐÄÐê PÞxâ”C§ÙßN äøÓP];çáîÔ(ˆ.À·®y¤Ü±Úé«rÔeÀ¤qTúo—Ø%ú6oÃ`9±AÂ;¸[ÊOÉàÜVÏ%-yò‘„mÛ ß ¼;õT¢Д¥¢ï“¿`ð}ÚâV’LÚ;ÛOj•ìÄÙßÎôãð› œ^Ò1^‘£vUĵڨb^E\+ 3 ¯~-øS–Á«8ˆä>"a>Û‹ÕâÜ>bWФ=eW‹Ý©Óì±aiU¦Î]éiÖþýf¿鉧¬¿‡¨-$‰§µÂ”ç\+‰Aï9ØLÚHY¯Æ‰c·Ä@bœÆKÙhG.&ø!Íî–I{Ê®»Ó;˜ýN¤V§˜%‹þ)7¾Hs§9»4Os §¡bÅ@ð@þ‹x >g ®êëç¿.gR^Ι[ë²pGŸ†4G³‡Ò6û6ï„Á¡]>}æu bþ9P>§ôpn;G¨%0!|~0\ÕôUÄU–1 )«äy ñ‚ÁOQûý ´w4ûéx€q•¡ž@\”,Ÿ…Iêýk­;Cª"~»÷Ç7ù™ñ²r0Þfæ÷´Šy±_*®ØbÁ$¤5GÄæNéi˜·}€Y‚dE›Óôê.öøôJ¢# FÛö¤ÙÜävò<åôéf߯֠ â`iSLT‡Þ3Jä9‹ÐMl5 ¸1†ñx‹"н°Õ– Ø hv{WŠ“óî.çið]¿ÿÖQfäŒ0TÖ6û6ï„ÁÌ‘b]=qÂp¦ÝßÓì?b 4JgEó§ËvUM_E\U¥MY%ÏSˆ ~ŠÚï_褽±mÐR«ù§ã½\÷ÒÑN;ñtW*bæFl†nfö€*7`©Á<@cUÌ«ˆk%A$ÄŽoº…hÞò'E`ÎUbƒUÌ#Þ«–&àLl¤rØãÞ’¶E3iOI«|L?Ýì"µÊ¡j4±Öôº€ùdÄÕ?þqóÿéOÄÐj«2yÇ?• ÌÆ~”ÏheËä^'ç´æ _œÒ<Û†·ÞÌ:¢¤Õþ]~ˆ ³`óU08½HΑã]Îmçµ…ò¡WwI£OCµMCúm#RhÊXÖm ß¶iž+ؤ½£¹ØOnj،q§€Ì¬yT3_ˡʔÍÄǽüÉÉßyº(®Šyq­$ƒ¢„Ún¨¤&#)<̯ðŸÕ™;ô.Î¨ÅøknrFYÃ.9ÍpÅ¡õhÄØñð\íBüš¡jx;µ¦ _Uƒk ÔNör cE¯U•¯7ãè G2åTq.1ÙçÿøÆc¬ŠŒ¯çÜ2 Žoð ëÉ‘Ì=ç÷ó6ï„ÁÚ¸ƒ¶ÐÍ›7þåu}ÓS8‡q õ_|¸V0 IF»SxUÓWWÕl@SVÉó⃟¢öû:iï@l{ð©Õ¼œÎþG?`g.&ao­ã'úÍ*¤tÉ϶~âRq0)]B ,âæâ *ÏÞß b„ñ¸þƒãC^&& ¬Šbæ¤=%ÊŸMxÌ>¨}ó¼Å_ŒH|4õ4ÇoöYÁÏDŽ””Òœ]: ÉÍ~”à7$¾|Í5¸‚NÞþÏܸ㖞VqNo4ÒŠ¥R<Ï‚óy°å9ήãÛÕ߸wråoôPô›§Rìj¤Ã2Vj‡[ÔŠAßÉ+‹ø Ÿ| YÛH[­ªé«ˆÓRNÓšòT†§,üô&¸§“öŽæb{ð@:ËûÓÎјáT mÚΈ˜Ua鯊¸Ä¤”_żЏT¢‘Âô§×ÿ|µ·’—[®?.¹Ëଠ”1š¢tKs“+42ÿÔìyyÃ1¢æŠ€„qÔ—:‚ÓìûU–ݵ)¶Šm%bxŒe?ñ·´F@~¼$ÆÞZq6ø¤—¨Ýqˆ@×D¶\•åÓp<úQß”ç.=Àæý0Ä z‡€u Î=¼Áê8ÊßiO§UFUEœ-®”9 )KEß'Áàû´Å­$™´w 6“ZCMžâÒtšTáUĵz«b^Eì‘°ÁŒÃQ‡ç.hìyЉŒç!%-MÚSl]ÙfϬ¡I7;OgÄhÇàè—3Œÿ²ÙÛ×^ÝYãîtÇÍ6˜qØ âýû‘ •ø×q~g{í¿mÌH¢Çmž(Ig߈å°ùSÌâë [~‡†;Y =Â~vˆÚSBMa”l#¥ïg'š2­È=Ó ß³]ž.Õ¤½£¹Ø60¦ûÓK§£1© ï¿v^Úö_R ÀÑ<ªˆ·+‡9;ýU1¯"®•$Ò+5žÚ‰*â_¬Þc,åäRÍM.WÈè<ÛìC,÷§Ïø wF$S! áa!÷WEœ2Éh˜_{†š´§¤•=¦m³ æwgö¼Ì.4‚Nv—âé©Ù…é‘#9í>•–m€DL§1-΂٢dµÀ¶ÛV"ÒGn§œã-Îį§¿ ÜÆ‚²·#62›g„) 2ñÆ6 ƒÀŒŒ$އ ƒÛÎQ-1QH# b‡¤gwqš^=¦c󵵓My¬ËÝr ¾[‹ÜDžI{Gs±m<'÷ ‰S<ÀXÍZƒ<«ÏJ³XÏày4’*bnâcDeú o„m®fœ>ȼ«$ÈF¼åÛL£R_^™Áb׋¯ºðŠÓ+QAÛæÎQÜÒÜ䲜i›=¦‹Z€L;¯o˜Ù·a…J=âÔìÇÔ”ö¥O)Œ¶Ô§¢$xG©,½ƒFÖâ±xé˜Õà$´U@ÉcÆI: ƒ«8˲r~|ã9±yi‹_ޱß÷Êfœ·Û!ÍlÞ†ÁÊ=½ç)-V^ãý ö ‰0ÖÀ‹áÑF†ÆCª~v2 )+ùòƒŸ ôŠœ´w vÛ'}”æzO¾hÀLwã#¿% ò³3SsðZlÎüÌ:È|œ9úIBûr0}ð>¾ÒÚÝ7UQ¬Bq[mÀ(&‘á11iO9V$ͱÍ>?®"Ö:5êâ©Í°§Ù§Â4OË8Á*‘³Ñ§ ¡FÔ ““aΛ¸EkA‡PÒ¿ˆ¹å—{cq1QÅ9Þušày$¬}ú¬@w`¹6…+‰M>Äà|@{\©7J`óN ¶ç)ãx`¸“¢@ŸÑ!Ï<+qj(0âdíP%ô©Aîîíd'”2 )wu¹áé‚Á7l”;ˆ4iïh.¶Œéþô’s4Ö Åú&3c¬Ü% ñãóÁÁ”-'ó/Wsyôö3G¢¯Èee€ùjMà:{Ñ 6Õ4Š`óN,´ÿ&üû^%œÚ0˜ç £vU—ÔÖaÄP0É»‘Áà#…3žóðȃäéÖÉNp@Sz¸É¥ƒoÒwcÒÞØ¥¡þš†Oñ€1ãÛ—üx xr4ƾofÔ¥ŠxÇç4¤­Šyq­$,Ú·tWX‹77ƒò …Í?·EýîdÛNÚSvµØzÌ­j§ìW§6#ÑÊìw"µ=}3Î/_Aïð6ÿ8ªÄD‰›á°76Lï c"½úHš&Â`ž£>BÑ< “PÜH BSÑYÄ8Àæ0˜à)\ímÜPÕ!FëËWÆ =zˆstöÍG÷‘äôÀ0]É æR”9&lzçÕMé”ä‰d ?Qùw.zÒÞÑ\l°çýÒU? ÆNX gôãÀÉc;jj‰S#d0Ç”æÓ7‘$™Pâ'œŽ““N§Ä€9ÄXJ†­TVs“ËVapf•Ùó‚ÛœƒÎ1 W™}×Zc2fOŸòK‚Á€ˆüôý(ñØ¿!Û-¢>¦% ¶)À<º7g&½ ·ö‘~€Í;a°ö¯F¸ww Îéèq¬]Uެhw‹g åFF†bº”ìÏwüžhʆÒvbµ`p'ÅÎÎvÒÞØ)æy¼ªð@ ñfó¯ãdcÒó :ˆy]Ž€@â.Φæ¿ÈÄ~IÂì|`HÄ‹¿¿JLR,s•ƒD’}ž<5§I{Š]s§ÙzjJ£9Åñ˜5xe^7{[âÊ«HžÜzÚÊ~öô#ª­ôÀðXøö3iB©JÈ·Écó‰ÉøÃs€¾äÕ`óN,ÛÛ]ût„6®ùM$ÃmT=öþ†×õ“My]¸Qw.€0•(èâÓg0L+æ]%AfôæŽdÆaI4û>>˜ç ªmn.N:¢¢Ú úæ&g”5ì’Óìß–¤_^YÒåÀªv)Ù<ù~³ïUY æçwü‡H¢ RŽÏSpQêËÝ<¬qè…©¬e>Rç^:ÐS8¢U{nb„ŽôlžÖ±Ñ¬®ª_S;)<þ÷ÂÙÓ¯µöäð.…ãZ·é‘b$áPä¶Mßéꀦì$yC¶ 7TæGb5iï@솎Ô‰Œy¿tɃ4\3Ôã&=0«ˆ£­2Þ†Ò8pQ–øD%ª˜WÇ‚œ’ˆ9 ½s—ð672`ƒzACÕxcˆpb–†KugÞ§ã3è•y3sˤ=%ª=›pš=šä@¥2rÂ!”ƒN1û¬H­2iÜø^•vGÁ<ì§?V0õA¶I„Hã—WÖ»9ÂÓ嶉£eR£*εÀ†Õ?)T3Ûµ ¶÷‡Á<Î¨Ž»_·#¤@ÆN\ã’Ò¸ZVìG$AwN³dÊìg'rø24™½´`pV-+sÒÞÑ\l'(MúFþ) fDeæÒ®$4Z’ȘUÄ2oæD¦¸ÅÝ¢´ÊÜ·³ÿ*æUĵ’DÁX·Ex¢:Ã+*zmóôÁF\ØÕÞDª2™‘ULoäÉ!¸…5bÒ6xhnrQž'&œfõésjän#ifš>5ûÞµÆf8ÃXŽÒ§OFrz#9Q• “ ‡Æ—7‰-甕Ö)ª8×j†¢©šÌž(h$Ïš½Áöþ0¸ æ@“i†ÁFóÚ°/¡^Ú_¢|áOÚkÀÌã’`³AÜÏN>äðeh2{iÁà¬ZV椽±Û>é;ñ@:Ñ;Ó§x€±ÆXÊd*翚²öYEÌíD*ùHà­Â¦y£÷QUÌ«ˆk%‰˜?¾\Ã;2/¯Š¢ÀÆÇ7MƒYvù’õXq„úp†  ÑŽÄäÄQ’I{J”?›pš}ðv~ú üÀÆÁ§è¹Ô NÍ>+OóLÄ Žb{ÚÊzÔ¢ëa]¡Ê_¾ûH”êSwþÃ."efAQçX„'¡JA)|ZÇ,ÏÈdwu€ÍÓ@”ånú¾M“½ÚÃ¥¨øÄÍ@ʃÏNié)öƒðŒ6ÊŒ7¦4iºŸ hÊ´"÷L/|ÏvyºT“öŽæb;ñ@iÒ7ò¯Æ§fj91‚1A&Œüô9.ÌiÖN™”Òóã-ñ5I38Æßð±Ä4‡aª =«öê›»«¥ ˜M)«cº¹É‹Ÿã4û¸{0Í´ÝÜì›k\ap0 ³•-ø}Idm”PÆÆ*6VgïëRÅ9áI¼õ¸ßxHá©$X²iöYž·õS)ì õf®Ioî –¶—…{A£SV¥Êlæ)I§èùYvò!‡/CóÙK gÕ²2'툭٪U :ñ€1ï—.]ƒÁabrOg1ãvô0ÇDÉÕœU¦ÁüHo_“„‰†½Ýl@Å€LÄüãwPJ\³Ži§1èN9xÒÀKéÕczÒžr¬Hšã7ûà§ÂAº¹…ùE½Sl˜†IDã¡ Ñ/—*áZ¶ôÐðP°©å¸žÚðî¸íNwÜvWw§;âã©- XëxKic(Šf–ŸðËaK’¤èÌ”õžÕÜäÞ?ó•ÙÓrl(ç4_Üoô;ŒMCÚ9„Xâ<ñ–Û³QhöÂÓJ Ïà4K¦L*EüpH³¯òoÆs$lÞ†ÁòÓëõ,*އ¶Œ°a°];)ä.¿õX%ù€¦¬’ç)Ä ?Eí÷/tÒÞØmŸôm<`L÷§—üx€ŒÇê$ëhü2©öSElðÉ^ªb^Eœ-ÎÈdÖ#…pà”v7¶¥ò+ß !‘*Ú³­ý¤=ÅÐ-—l³'þ`Z*N¸)qÔ¤ã¡SÃøýfoKøÈUP˜®¤ÍCBŸ*l‹¦ÅºÒØjZ"F¤~œ/Ô9Áí7¨’g™„sûž€9ú½í¯? °yÓß©ÝåWäÚÎY­¶Ê¬µÀÚr4e­Hãé ¯ó)Jœ´w4ÛÆÆtzɉ4±òY4›¸Íỏ‰*âx—3QżШ)@JÆ$ÃyñS®‚ucf$®•V¸ÅXP àLçÈí˜hnrÇ"ÆçÔš=85x8;k•õ^?Eh31@­œv±X:V;‹ /{ˆ¹«ç(’3¡½h,9†dïh…e¦í{4ÄËïÍ)5ÍÒ“9Àæ=0± ¯oéœJU»[~­ÖÊ? )kEO¿`ðxOQ⤽±Û>é×â'€Ì‰4í²dI„S±8mJ&TE\bRʯb^E\*ÑÈgš~[ÃýÓŸÂfP ¦é­ï”&D~êÕx-"ãI;}ã 'í)†n¹ä4{œrÇÀ`rŒ^à4{[¼¯ ÷‘ŸÍÄØCû9Z¥€“yJQw±çÚ*ãþEÎpl‚Á’qkg™È]LM"Ð"HV!"`ó6 â²Ø$óc<äq5 zi»cþ,0ÛÙÌZ Ì2124¥QúM.-|“†¸›“öŽæb;ñ€1ï—.ùGã°Ñ}_¾âà`j3¬¥ŠØà“½TżŠ8[œ‘©µiÖpñt1²ç•Aì‘hÌÌ+Þñ%îèÜ3˜779£¬a—œf/k|ƒ[I„pÉæÉ÷›}×ÊÊ~g/hŠu„lqH ãdù%bqèÇ9+¡‘‰Ø¸v†ßÞ!/{¦¨>ÞN|®Ó°‘²¹+ã›GóG{ÌI-úò$id–ñ4&àœÕ@TÅ­,°JþMY%ÏSˆ ~ŠÚï_褽± 'Ƶ;ñ€1ï—.1¾ùEÅ·1{•¼:±vUÄñ.g¢Šy±S‘¡Š€7x¿éËW<]§š„@”¨«;@Å0ßeîN'í)»ZìN=f€;6€…´Ez”lþ>0{àáH›‰!yiw_<¥ À¤"ìS1þàÌ-;Îà®°±Iòwsr÷yROp¸CA†àÃð½=>ƒçÉ9ÞOPwÜO›GB”h ïÊ °y' æ·p’§0œÃ g"3¢ß˜¨xŠœÓÛN4å`](nÁà Jû=Ü2iïh.¶“¾q©j4&"‚-8˜LO!Ÿ‡˜uOü¨X2t<ð)Ó°póx»‡¸‰$±Äl‚€Ö²ã¥SBüE?¾…·æÏþš›ÜY#®;Í×(H\CòŸÕ<Î\n!ɶû³9Úh‡¹É1=P>4Ž?g53iOÉÖ%f:Í^`B[닽ú5ìßoöQ˜ @Å›ôå•¶~CÂ<üfÉÛÒUæ´V¤ø|œs+I@ø<´î¸,iÐ]&§l;‰ÀÕNèUVuX$ã$’c–ÙX .w¬ë¨·Ë9ýìd@S^®õ° ¦ê¹ š´w4Û‰Œy¿tÉ?3¢âÉá] V-C˜kas'˜“˜…Ý·W·}SI+€–éÃ0T'óûHª½Þ‚Gi$8¨‚½ï«fÖ¾Ñ ñ'ÐÛVÍMÎÐÿ°KN³þ7´”%›'ßoö]+«_zqà & ÅQ¾‚w,—|: ÂfHpjÇE9”`ðãœeÙ9%Iè C°}poë ¤ÉÜm”!ælÞ ƒõ䎜áØÐ/ϼYô3áÜÃôöþçÙhñÖõ¿Ÿ hJW ŸJ´`ðSÕßÂ'íˆmƒ–Z;ñ€1ï—.9ñ@Ø»` êcÏ µã4»¹U«"–*`ãw?IÀ¼4GT1¯"î'‰6 ó9B"v m‹fÒž"áK¿N³ç¹,¬Pl‹o‰í´dó÷Á4+ûì©ú„ÅÒ¹H“yt~ 7ÆQEó.©.›»Czµ 甡'•„©;~<÷ÑŽŒ JOƒßÑoÉý°Îף̔‚~y‚`¡!¼ch¾ÈœÞxšîj'rø:UéŽ`ÁàBÖ©40iïh.¶ó~é’s4ÖÈ –cŽÖ,Vš…«ˆÕÐ ×Ѧ£`’íUÌ«ˆ»JsTÇ@§R¤Àˆjv5Ý]ÝîˆuÚÜä²¥ Îô›=0 0¼;J6Œ¬Âëé aâ"è GÔ'K¦k¨ ì>Rj&1Ù]mÂyÇóô4+ wÅgaA8 1}Dð#Ï6Ï𸃯ÙS|¿Nôo‡séIÿXÓóœßðk ÐãCxÜ^Ù;¿×GÑÕN4¥¯–ϤZ0ø™Ú¿qÙ“öÄ.!œkÊöãcêÏ^rÂ`ÄæM|Öþc™£Ã$eîeTE™ãÁà H¸!óûH‚+8àœíÏvÔèýzTh~'ìÐVT“ö”MÅŸ;˜}Q¸èM€ÞPú`¾|ÍŽ²dÐ=ñt«–£€%ðù8çcYvŽÛ‘†×oñd†àå5Bâ#™rؼÓ‚öÑoLøÞRõÓ|p8†Ä¸¡0*^EäÙAÃHJö`ºŸ hÊë>àöƒ(yÆ"&íÍž`\Õ%c,Âö$UEŒe†ØÈ-<˜ÁœDiç(Ùpó*âÞ’ Œþ€ÄzeþíüðUŠxc( ¥—W[!ÜÝÜä=!ãfßµÚ´,á1 ¿ÑHvEGKæ.Ì£D¶»+=-ÏÇ9§¥xÒç¾Æºÿö!9OíؼÇètÉüè7&àÜÐ̘̺û 0ØtVìèOOûÙÉ€¦<­ÝÓ  ~zÜS€I{bgý9—•|<ÀHHÕÒÚÕJõ:—(qA¬¸Yú³§‰#ó*IŠÄ%QÕRf%÷²Íh÷Îq‚;ªqÒžr¬Hšs³O¥j˜VËbç¶©«Dh0õž[v¢>±¨]¦NSÎY‚æ™ ·#O$‡“^Âûâ]lÞ ƒåŒs’à{ox^§üFôm½Áݼeì4Y"sæ?n¥‚4e©èûä/|Ÿ¶¸•$“öŽæbß´ZwÃçÉž`»™éƒ@§AVIbßG*®Y µð¾¼­‡æ&g7æêm;UõiY‚´[ ‰ýïJ,ÄÉl»í)vhGP:…-HRWOqQJ\bX›o×+å†Kü×éûW瀔Çw!`óN–l~û Áàz‰¶Þà_JÓfãéy»ôe <a@SžÊðt‚ƒŸÞ÷`ÒÞØ§ÓM•Âo‹JˬÔ.Î}$bºTkÍfEŒ³—Èìô—ebHr¤7ˆo" ñÀ@Á$¤:Ö"Í™´§¤U8¦oköGQ¯åð^±Á䶈 N >²LWØ|o»Ë ¦+aä"ø§úGÛ~ªˆB³—ƒÛâxƒµöP€$ú@ jàåöžWv`óN¬í‘9ÂÝ#0Ž—”èä ®Òv¶½ŒÌ* 4ø/ hÊc¡wËY0øn-ry&íÍž-(½tÃ\ÆD€ƒ—¹˜—Gu¶E1Ùiƒ\FZˆq^1§ÎNYV%Ij‰o"‰fE‘ ¤L«ÖÜäRæÏJßÖì›(@H+ ÝWHpj D–ÚyGIQ 8[Ë·'xtçðÉ•ÍuLÎG™«ˆ·»r~|£v ,gðËaof¿}šœ ò¥y›ؼÇØ`½(÷V‹ßú‡0Øhk—nD•Ú>2°s<hs(]Д¥¢ï“¿`ð}ÚâV’LÚ;ûwâ .OÍ Ì€l+¡IJô¢º@>þåUØï¸sT‰II’,½A|IP˜GñÏTáïÿå¿f+3'í)QþlâcÃà™ØÌ£ÑÉ̪BÈ6½š5 XÁ$¸·“õ­ù*l«ˆ·ŸæàÜVÑ qç9WÛeC@ bõA ”Â]ÙWDؼÿòêo!ÁÁ²!Á 7÷_Ðöi ¦N Loñ§4¥_˜gQ.ü,Íß¼ÜI{Gs±o‹Jx’™‹éŒÝ$ Ð$h[¾_VNåã삞·"(µoçjI’ìñ$ÑŒƒ¯n´lEbæã&ÄŒùƒ±cÑý·5û&UƶÁK¸jÁKh^[— ^Ë’7Î^VX Κ]†¾½I§ßà:Îyƒ«ˆ/T™r)°M;êv}׬TGh3~O¤äÄŽ’V`0€©T‹ÒÔy¹§Ð:@_&efpŽìêóQcrnkö­ªÏ®²7šø„ÿßà\©Û7XjÑj‹Aœ^: ¾LœÞXJcZ‡í²Í-¼Ð4ÁÈ|ÃìÙW°Ä–üË6oðÜ]òÃ`Ÿ8DG”°òáìwòœžViû”ÛŽà²îøO4å±Ð»å,|·¹‰<“öŽæbßÄ…Ý †ÉoÆ#Lª$1ˆo"IHÛö°$5r€̶Y-]09ÜnéÀ»`pV±]3B4.O,úÓÐÙ¥ÿTè'ÃÓ"‹,é%#ÍS+,Az©Š8½±”æ“1À']Ö¾¥~ç»0Ç[ RÒ©¬ñwÁæ nÙKNÌ«gÒ_Þð50œK´YIœ™ym;ov]³ÀSÆšòT†§¤£q|r|ºTK€§k`ÒÞØmŸôïƒyûfÛÑ‹yüq;yH]Û| —$gÄU’ð&`:—…o`ùur¦À,žüèÝßS`‚Ë—°máÞÝïò?nÌ6=†·5Ër j(¼õöþwj¨ÄÒ°šÀa³…_ÖŒßËyô*gL+äÈz1 ïb§=ëHIŽßæ³·{20Xë5t%yé©ð° ƒ£6<’8iÄ3¨îç÷S:yF²Z Œ7ž&4å© O'X0øéMpO&íÍž f2ã*PŠ‘°ÍXÈœ¾+”ð†¬$UÄ;žþS&;…. ïÂsjÜû¸,‘-âÔä˜Óà‡ú§ gÕÛ)ÓÅPéVŠ›Í´ùNW 2mþ~Í?ÖêE9Ì’Hƒ¼ÊŒ >ÙKt ùîp±dit“`ÂþNOmþxKmŽ£@@/¯R5¸‚¥Õîä ¦‚~Vh£Þ+˜iÊ*yžB¼`ðSÔ~ÿB t=”€ØmŸôŸ ƒ…~Õ™ÎΧì Ýx‚âAÄßx¨"¾,2ò€4XÔÖß8Ͳj¥Àk08üqï.Q'ÙzõÎ|®Ù÷®ü1 ŸvÙâ·d?¸îµå$8ÀÏ6²…€F!E ˳•‚­ÇŒ³÷žfÂaxHžôþó»B²–ö†Þ·oÉ)͆Øf‡*LS‚~i oPéÙ†imç5D•OÛ.\°Àx¯'1 )=b<—fÁàçêÿ¶¥OÚ;š‹ýD<À>Ihx¿ìmË…?ý‰­4"9ÍØ̾Ÿ %˜Á${·Gó .Õ1ÒG›9ÍN Ö Hxû´Þàƒ_^yþ-a`òá\j‘˵¨U`mA ¬å)úMyM°‘w-%Ø¥›³;þœb'”Ý]÷êØågOá¼ë†ÇÒksjXËßcµŸá/îÔæý¬J”ÓyƒÑ6ñèác8J©¦§ù~ E‰“öŽæb?–TA³ü*ñ–.¸ŒÌÆÁü[š|w¶§Pð$N L—;šô4'o¯Ô±øKÂ^ø«"®•„ J!î¸íõ”Ê©t­¹+Š!—NPfAÛ±¸Ë&GYi°Ä‚ÁQ¥wKÈ’ƒåû/€a N%ºÈ [¢æñø¦N'ìøT¢¿Àá¹øOÒÓ[½]`’½å²Íg¹e3»ÂàæÞ`ªÀXÁ 8¿¿¨z5)ë‚fµzÌДÇBï–³`ðÝZä&òLÚ;»í“þa0ÓhôÅDXÜ,l`¾ûå+UÀŒÏŠ)’_mæoX œx %aTEL¡U’(Ú"8¢an/åZªâaÆÙƒI¨²†B¸ôxOQ°Ä.vÔ.´÷Õ'š}ïª]àpè ¢cü^`²»…˜%@¯ºÞîR«S:5Hø-0ãËW–Wú¸ÍŸ×Ó §T(n! ƒÑã3Á迊U–¸‡ª M™­Ñ­2 ¾UsÜG˜I{Gs±oˆJÓùº„óGoˆ‡·u| ³ã›bº·ï€ æ8K)Ŧäj±¸9%Á÷‚Œûšð NO…‰%f~ž@}ninrQŒ'&nhöOÔ–}z$8m%LÀH‡í\àŸÝÚ·¶Pa'ÀvˆŒ:”RË-¥`ó]aplÍ´R¦y>⡆_$`¨Õ¥9s{? ДW¿7‡ƒ{kxRþ“öÄfðo¨óéð€BvñE„PŠOŸc(oI' °B’á–OŸqh”(Écþ¾#ͼŠæU’¼UpÛÀJ55ľréçw¹Ñl…ÀyÒžbëd:³·«óàUõ ý8°Ndo‡Op<Í¥À¬Éf*Î2cJ±ã¢HžÄ›ï ƒÛΆÆZÁà~8 ) ýÜäÒ‚Á7iˆ»‰1iïh.öð>Uy_™+I0µÖÂ|‡#‚·oXueMŸñ3aw7r (¹ b½¤ÃBÞŽL§C†sIngJ sëöú‘¾Š˜Ûk%Alb 4!~˜£ 1§J w! ~PG *)Dü››\û‰‰;˜ý«¿+àQ ãéMap`¸íõúõö~+ü›ŠcÆAò-†Ÿx'zk+ø7Àæ»Â`F›]w:mƒ;Yà€¦ì¤Û†l n¨ÌÄjÒÞØ­†zµæð;E0—1/„w¾¶y­4Œkß$çfRT=qk%Ë½Û 6vcƒƒ†ôÈMwÅÓ]UÄÜ[% ¸—zñ†l,Ï/žäé©_Ügí5G¨À9uI¹íÒ“ö”]-v§w0ûHÏ:• °Ì-è&!Æ ÑŸ"ù‰üW/HUKM`0Üx¶Õ>o§CAU…Ø|WŒ6ªê{™8È—9pcW ДÔ}̽ Ñót¥LÚ;š‹}<¦È—Wf4üŸa–üòµ4Œ |‚fµÕ¿a~Kæ'zÜP`?\¦@ÊìÎcº]3i\ZÕn½%ˆXE ÿ*I gŠ—·–`æà³-¼6(Éý LÑ8³/Ú—¶§g~››\äüÄÄÌþ‰ÕO‹Öãf=pà°å±+%x$Xmß×à™N}œG<`6ÀøÁ¿ÀƒO‡‚ªâØ|WÚqÈŸ†µ‹êjšòÁê¸}ÁàJž±ˆI{b— âµV¸`fdZT½Hpjü±úÉ” Jds§ðáÜ éíawˆ÷ ÙHpozu—-ÃYH˜÷îÒÓ*bnôK¢É%ˆñþñ;û#~jÆÁ·ŒðUª i¥ŽéI{ʱ"iÎÌ>•ç¹é`™Û§rß¾–Û4(‚~D¿ãQÎî×µ%ƒ±µÝqÔ? üE °ù®0¸íaè->Œ4žKý,p@Sz*ø\šƒŸ«ÿÛ–>iïh.ötx€ÉTц¬·â:]ÀeFZëàfä’MB©-žˆ€†8Iî*ýU‹I•$ª£¾½à(IR›ÿ†Æ·oP_ðÍ¡¹ÉÙŹ:ÙwU }Š®Áºƒgé¡J¬KÁöUw9‰Á½cúL0‹,'9o<%`ó]aðPopyD=Õs$ègš2ÖⶉƒoÛ4ÏlÒÞØ ©†ª›h„gØDÎ2Qȶ¡V VNIv˜Tlð¬º$Î(?ÒŒöí“ö»RÓ™½]¶Wö–l2²ýüîì¶™{“,äÔs+Qî¤1æäâCÉ6ß7TÅ©y€:¥©%hhš²¶vãé ¯ó)Jœ´w4{:<ÀR>Q¾,†ràÔ¶7&\Yíå aϿ¨¬ôAÉ *ÿUæV v§b—eÌ\!R:(aû”¢#2DIVs“Kx?-9ÙwÕÁEöÞ€—KÇ[{«ú,†G`÷—y¸q€Íw…Á¡ƒwøëÄIûYà€¦ì éÆ, n¬ÐÂnÒÞØmŸô§Ã¬r0½Ú_^ŽvÊkqBh3(œÆKǘ“wØu XJ1Fþ*bʪ’DÄ¡²ÛÖÁ¶ØÇŠ9ðDò°šü¾=²AÌ¥I{Š]©éÌÞ®Îå«|#†Ç"v!–„Ž‘ÞèOq 1¶‡Ñ X`“"ɘa3Àæ»Âà¶s„ô ©nOÐÆÀ˜Qh9«·hÊråîreÁà»´ÄÍ䘴w4{:<À”Mt++q,œÙ±¾Xc5s®b}9Õ¾Á¥\Ä,ãr ˬz‡ß¬åV_“„ÚiCŒPMs{䬄¥LXñP ·õ àÝ¥¥ò››œ]ܘ«Ó™}'µh‘·"¸¶g.ž’Z§í©éªQ4(™8-i»Š˜‚.HÂ]Ú Š’”ÄÞÕÂ>{Pà÷‹Aö-“ö»Rs™½]—G®bÞ œ¿ùÛÿ 4€vòÛx/¬`¨Ó°âö>&±ù˜áxÛ³l¾+ .Z×ôÊRTXzûô™‚±y’·[Io Д״:ò®ƒGj{¢²&íÍÅž űRý1÷.hŠ¿,Îro¤ùMbsk0ª#öÛ§lM\ú«!®–¤Th.ÿ‚aÃãƒöLÃ!œÂþ\ kô:Ë÷˜}VÏOɤûÇ(q À3TCH¬xà·ûé3§O©fm¡Í‡Ù£]a°ý |Æ“Ã(ŠÿaGùø¦½-p@SîtrÃÓƒoØ(wiÒÞØ§Ø²J½6 cÔ¶I/ª°°ÐÆË̸­C/_UP„!Jàë­…ÄXýÇçÉ:¬¦`Ò!î·ƒCoCC·9-aì*âZITw\gšwP ‰~¸ À·çˆOŸÑ!ÇèÓž4'í)† qÉ6û*;ß{ÌÞ–mäU¤å Dô‹Gź03Þ®"ö˜eôRoõóC9ÀæQûþê/{,¶£5ñ$ð?¿ëá·€îjXË€¦c“”²`ð#ÚûÀ÷NÚ;š‹mãÁ`@ ƒ)ÆÏ¹Ãé©g4†?<õ‰7ýrJYYÃc¦fbåjÜ$M‘®öæNÐÇÀZn/Å=jkSð-aoJ³vq¬‹Sѳ=2ôè•’àÈ‚Õ*Š3¦jÚŠ(h8£Æ(ä1ÑÜäŽEŒÏ±Í>5ãÚ´ÇìÇ×·Tâû)±:æ«{’/cSçµíÈä)9l;éá çìXñ &(´± ËŒ!Á«ðòÊ,ð箈`šò‘ê¹wÁà1zž®”I{b3›4Ô¶â€Lø ÃK.ï`˜›‰‡Y<5@‚@sÞ‹‡À籂ìã¤]ý‘'­Æyc(D1®Tb-¸…oÆ‘yÌA¢Sn¹±«ˆk%S:ÚÇ/_³Õô+0­i!“Ó:B9iOQ}K¿¶ÙV}z‰ɶTI’çæ£g¼ §lžõž.ñzŒíB)=n`óØI?ÜãA£.X/)?XVW Ä64e lËsÁà¶úü0Ü&íÍÅvâð0Ëš }'x˜JÀÀƒ áHJSgš£4¥P.á prK’#qš#O]ͺð‰B”£ýËkÊj—®"æÞ*I˜\ð9kŠ!ÁŒ“ýó+P·¿©÷Ç7½ÛB7ÏvÇjnrÙŠ Îtš}ɶ|Ù®¬QÒ‚QµÆ-²`!¦ÙÜŽ—°y:,žCСΌíÈá)9lÍ÷ƒÁ=¼Á8=ˆ ¯W¼¼FHüxëô¶ÀMù¸zsX0¸·†'å?iï@쟾wíåÁLaš÷ ö¢ ~°1;G*µÃ3œæ(Íì=Æ1Qr§· )B·„XbsŠ—C·ƒr¡OYíÒUÄÜ[%É®¬Ò©_‘PXB#Òv¼á‚ã¥lbÒž’­KÌô˜½aÛÆ¥¹`0±åô‚᱄¨»ZýaóZ:çr@â¸;q+þø °ù®0¸í!%kا5šœ{[à€¦ìd„ Ù.ÜP™‰Õ¤½£¹Ø§x‡!>Xm,ð ¿¼2©›àïbwj[]­_Â?†3•øg“*bÕÈ/‰­®î4¶;ÝÝN¹xõyap PÊi››ÜNž§œžš½aØö¥¹`0­=l´FÛŠü­F˜§–æçÙ›r€Íw…Á­Ú.Õ3ƒÿÑ-Àër½¤dµéÞ8 )k«<ž~Áàñ:Ÿ¢ÄI{b·Ml<@÷ù}q¿£ßè6 €±Ãž¢¼}üã Å{¬ˆÑ>¼¶ìþ«¢ïGŒ¼UÌOëwY§œ!˜´§ØU³ÍÞ°êÓK~³·%y•Õã¾yÿSÙ‘bܧ¬6ß·#Ô.¿Y¤ûù Æ^f‡wŠó~8 )ïc·%I .iæwž?iïh.¶¸`ð;úeMç0ã¿§HØ(‚a#lYÆë`…"d±@>0¤ÒÿôOÿhCVE߸VUÐù[¥@'ÏHÖÜä"ç'&l³?źßìŸXý]ÑØ‡¦Ô¹ø%¦w–†]u?`ó]ap!WEä€yÇABÃ5áU˜MÜÏ4eTÑm ß¶iž+ؤ½±Û>éÛx@¯Åñ˼OT°F?ý226ÁlFˆa®¬Ìò >ÚÎÅ ï ˜Ásµ¹Žµœ*ú~ĵ’dëršéQào˜lß?E™„»püæÒádÒžr¨Ço2l³7¬úôÒ”0øËWBĵ«6N`¼|ôîVç7zŸäd€Íw…Ámç5š*З*†h¶š!ÿñÏg¼1ïfšòþF½`ðýÛè)NÚ;š‹íÄÚ$çm$|EŽÝ TàÄB¶¼Í n sH0êRzpÿnßÓ.‚÷¸à:®¢ïG|Aò´Öž´S‘ž|dhPÚN ‡xõ˜hnrÇ"Æç8ÍÞ0ïÒ%§Ù¯²QbDMoAP/¯‚:¬øw}àKl¾+ ¶{ôµ†ãY›%…ygÅ6ðòôtŒ*¿À¿ŸhÊ õ|Ë‚Áƒ>Kq“öÄŽ#FU;ñÀ‰qŸ-ÿaË… a'`c^XÊpɲ³(._Ê:VMŽbêN"q4~¾Šy1‚ÕÒëbç8™Ð|(œÖюļ+/e“ö”l]b¦ÓìKX×Èwš}”ä ¶Œ`ì‹å_až;H8X†6ß·#¤| ¶ûPÚX†»ÖRý,p@S^«òÈ» ©í‰Êš´w4Û‰z™ýƒIè3gÃ`l&ì{¶}!BÌ)ËiHžØà”U}?bDªbžV!›®R ÄÚ&Vk§Ð^»4# ½ûå«à¹2k`¿·ÌæÃìQ]apo0F‚³‚Šà¯ÀSÁ~’ÇJ]Îégšòr­‡Ýˆ˜¯u0ã+wts LÚ;»í“þ) fa4<ûooCÌ‹3€UÖÃÀµ½QB Ux€J ’á¢déÍi<ÜÅê­“²*ú~ĵ’x*xMΓö»j§f_²êÓü*³·…v•Þ½pªí6ß3¤Õi’÷2àëý8 ¦íJA? ДMÔÛ•ÉòwUï¼Ì'íÍÅv₾˜ÎÿJ‰ ÆÜ’jr¥h€fPJó"š‹]…ÃD–‚~I~`? ¦Wj·†AmþÀ«´’ê J$”Q[<•(•_EOé¼ý§ð a´ß>€•-¢ŠNIî8 ®9¾ÓĽY.(0ËÇÈlnrFYÃ.U™½z—h»N ¤Ÿrx°¥OéƒãáQ÷ËWÒ$ î§sì¤ îá Ökq D Œ¼+Íè×ÐÎûYààák ,—»Ìà–º,çGºqR#vÃÁ‡uv[¢‚q½êˆiGsXP? <|­å²% n©Ër~¤'Õys±Ý6<­oïÄí~ xàÇÀ`’d]ÀNŠ( ã ¼\Å/ªä Ž"••ÿªè…lc<$ Ê"3˾Š~Ihœ-rÍéWY1”éW Á¤t©¹É• ™ï4{üK—üf?²¾vYêS¢ù‡?ügNIó°i,…Ø g¿:Àæ»Âà066ýÓ° KÁ`–\Jîgš2­ïXRmT¥·T•l•xR#v "^k)g·h1R±N BK ßpÿÍß¾m¿CD„fáRu(‚S7i¼½Šb‚$ ã aïYQEŒ–Kðƒñ‡$QÔS|²—š›\¶”Á™N³ß™´çÔoöƒ«l‡u…wB1ûíQ—M âßÃ¥6ß7÷ÓèX¡ X½€¡‰%ª†–ÐÏ4eª‡5°¤Ú¨Jn©*Ù>*ñ¤:Gì¶OúÎnæÇOŸqD$ÀâiLgµx@qÔ®ª‚6>ŒÖ+æœÚôQ:4$ìÙ¤ŠØ/ €VˆbÇÛ„ˆkh0Ô¥I{Š]/§Ùg Ûά5{[ÎaWS«Vèm `ó]apÕêl%ö®Ñ—w4<:ot’u²ÀM™Vp ,©6ªÒƒ[ªJ¶J<©Î›‹íì¶ÁYôé3ÞÖLÓÀ~<~#ÄW;5‘àÔ¶:ÐøüëOPz¶ö­bŽ ç¶o N IªˆáS%I?âP»š¿æ&WSx/Z§Ùæ]ºä7û^u«ç[elõìç»c€Íw…Á4è\Jïgš2Uõ”ËÏï üSmT¥·T•l•xR#vÛ'}g·e©]áÝo ïÇì8ñ¶ »­ÌrjX»ú#CÜ-Mo´Ê[êÈUÌU;Ð~ØÆm[#6$©"†O•$ýˆ©:$ Ô9WNÚSŒ†ã’Óì ó.]ò›½-áÈ«UÆ6R°g•5Àæ»Âà¶sÄ€Vègš2ÕÏŒ ÊGì´OIn©§Ôñn…Nªóæbû»-¸Ç£ü00ž$Ü—÷Âxù‚§BãªÂÒêy±âýŸÿ'L‹LJ<ÚX-sB á‰ ¬úIÌÓªˆ«$éG,] Ⴧqª‰ mCßÜäŽÍ4>Çoö†…g/Mƒ«Œm|K=¥Ä6ßÛ=ú)*5 íjš2­ÚŒ 3ï‚Ái#þ~Òƒ{G+Å"vÛ'}g·åc„"ðØwNã„Áoï ¿oŃ¢À´d–4ÆÕ¸§S øè–ì§UÌ5§Eÿý¿ü×’UÄ0©’¤qÐÕ—¯ |Žð‰„-ä;­ò±¾“ö”cEÒ§Ùg ÛΜW[ªÃœ`ó]apÛ9¢wCwµÀM™êg¢…¯2)¶P±ŽrZZTMëØ)=¸¥:Õb.¶“ê¼¹ØÎn 襳5Å_P± œx€†lŒ_(«ÝzaÜBv|#v‘Øp˜ˆ8d_o¯b®­€Ù± ÎŒHÿ’UWäJ’,qÉÓ%&³$ùÛ>l?¾1èá )· ÍMÎ.nÌU§Ùæ]ºä4û1Õô”Rm?¦“Ó °ù®0¸4VܳYºZà€¦Lµ:ÑÀÂŒIl!GxñçËW¥³ÓhZÁ~éÁ-Õ¯"qžTçˆm!œúðt[\Á¼Ðïû¾ÁzcŽ€ëãx ì{¶¹%a&,Á¨‡>¾ü»»íl†T¼Â\º¥ŠyøP<¿|eG }1¤Ä–ü*bè«$éG ¶çÁŸ_v„#öãôoÒžb×Ëcö%öó§ƒÁµ–i+öc\`ó]apÛ9b@›V wUò hÊTžL±ÓZ<%=¸¥žRÇ»:©Î›‹íì¶‚©li+ 0p± fÐÆ'ÉÓ(¢Œ‡ö)U±»@¸xÖUbv„Èý±o·ðË?ŸQEœãòª$)g™W‡¦ÜœÀÜ•å¶Ëlnr;þO9uš½x³Wg„ÁUöó”ö\è›ï ƒçòÓ¸ý,p@S¦Æ9ãÀBl0Ö˜Öâ)éÁ-õ”:Þ­ÐIuŽØNôâT¸§ÛzåñÀäàNlƒƒ¨?¿ëÛmñÓi%ù›TŸ˜„/óUVq¼×™(1Oß\ƒ†Ó†n‚ó q¶Ú¤=ÅV—Çì³(÷4sFlëêwxu€Íw…ÁMÉÑîš2UÔŒ n¥ßÌi}¦·ÔÀšÝ·¨IuÞ\l»Ûó@¬,ŒFp#Зx`¢s9e«øñ€¢|ƒ{ù啯$¿!á÷O ÷°!Eö:9W;yF²sÔ¡ï馫˜B°iÓ(L6ÑÜä²¥ δÍÞ°êÓK~³\åUœ_l¾+ žÎìošZÊM™Š´–TUéÁ-U%ÛG%žTçˆÝöIßî¶\‡ üx€Àcâõ!07buÀ3>Tw4? ¡­Úö–ê#ÍiN |fo,w•D:§v€a¥³²)³JxÝã,ßÑ6˜OÚSŒqÉ6{êO/ùÍÞ–p]}¢Ø|WÜvŽxbC<^ô€¦L…\Kªªôà–ª’í£OªóæbÛÝ–âÂÛpåÃ@~<Ì‹[“!Nf¬ŽÌìh\åÌ)Zø0‚:¿­–m–C‰¸«$ÿûÿóÿ†Wð¶gío–•M™ù1[åàrÆŸú™áßÜ䌊 »d›½aÕ§—üf?¬²« Z °ù®0øÂX«¢Yè4eªŠ5°¤Ú¨Jn©*Ù>*ñ¤:Gì¶Oúv·%Ø> TàǃǺ9pl" ´FÑGóÓ÷쀈„3…8Š/_/(¤‘{,ŽœqWIØÍLš¡Âø½³²)Ó£@ ¯üÀ0ÄÓÎ!íqjpæÒ¤=Å®”mö†UŸ^ò›½-áºúD °ù®0øÂøDmw-z@S¦ò¯%ÕFUzpKUÉöQ‰'Õys±ínË¢<ßË£rèÃÇiøñ@€dŸ>¬ÄH+à[Fx¢[5Γ ”âÂ_ÉÁ›eU"î* ¦RAÜÂ!m‚UÙ‰&DA°¹ûràaFuáQÂäŒBš›\VɃ3m³7¬úô’ßìWyç×À›ï ƒ—78¶õ€¦Œe‘XKªªôà–ª’í£OªsÄnû¤ow[Æj`«ÿ ½; TàÇ!øï¾ƒÍÒã¾uškÉÁ›½½Š8ËÁÈ,1G'ñí]mPl0ñ(P»Ì현Øà/_w™»ÓI{Ê®»SÛì «>½ä7ûHëô>`ó]apÛ9â>írA’M™Jµ–TUéÁ-U%ÛG%žTçÍŶ»-Åñò«ÿ$އ üx@{((bŒ½áà{Ÿ–XE|ÊmGPb.DâÝiÌWâžÎ˜ÍMn'üSNm³7¬úô’ßìŸRñU¨Gl¾+ ^ÞàØÊš2–EâÃ,ýLkpK¥­ö»MOªsÄ>Å-Umz­Ûòíã†x€žEì+›°qè×ÑàL|¬ôsªÆ*âç6³%! Å¿ ÆEþødXûÿ—ÿëÔ˜ý Ÿô….˜ºÂàÓÁm†Fh#〦L½6Ÿz†ÿÀ¢w@R©HóB4+»LN«ˆ¡gŽþ—ÿö¿Øoœyþ&çç÷ðFöo§Óúà–ú¿×“IuÞ\lg·%²”—׎‡ÑýÝ–c–õC¸[R¼¼rj[eìÎô¬ÓÎE?4ÿôOÿH)ó*bƒOz‰m߈ÝMsH;•1^q…‚ΣtÄ*ìøè´VÜŃLŒÄÎòŒ™ÍM.r~bÂiö†y—.ùÍþ‰Õ_EÛ`ó]a°=\Øuÿ`W4eª±; ,ÌqÌ8”ÒƒÌìÎKUÄÔz`sïòè£Ziõ÷éßôÊs ¶9Ý$çƒ[*)ù÷›œTçˆÝöIßÙmeðtÝQä;ñ#6<‰\å ôSœÃ8nR:žRÀ0 Þ×Ë1=0)æ@¾àóüñÌôUÄÇÛíT ݈¿%I`6 ÐCÁ¶† Œ‡îZ¢1´Ìæ»ÕÒ6˜Ü¨Â¤=Ũ—œfo˜wé’ÓìmñÖÕçj`€ÍTþðWÙãX˜Ï€¦L‹»ÃÀ¢Éš¨?¦fÿáùF" ƒ«ˆ5×ð® axaúxyM«¿K˜)2„átGžn©´èßmzR7ÛÙmõ9bƒÁNéQä;GãèýWÿæßÒOùtX˜©Û"E„>þésö¹n\ î_ÀäËëŸÿÙ_è óhóUÄÇÛíœ g"€- ¬¨ü›¿ýJk?¢ˆ³U)Pã;Ñ¡²\UR`,«¹ÉEÎOL8ÍÞ0ïÒ%§Ù?±î«èS °y줆'œéæ§uü hÊT“wX@›ÌÑ’Š„À'ófW3e0_°g\¦ÌòTÑzû;¾è­Y,®ä¦JSzpKøæLªsÄ΢¾Ë-èé¶ &: öO¢4ûóx€êÀï(ë,ô)âŽl`Æ£(Ï•l#ÆV$Øþ+[w=«Â\Ï­¿ž^s0¸Š8[œ‘‰œ© Jk4ã.`0M#['U ÔФ_6gÆ3Ì#€!—&í)v¥Ðm§³Gcëov ØöóøUÌSétÌ®ü¶ò?ÞX~4ëq*l’ãœO• ôü.™5wGDšV§Š˜C(]²LÉí)·4­‰)or¹”œ9Ðèéí+=@“ê¼¹ØÎn‹µs°Çz:`5þÜßmé¡âÌ ãs¶ €þ%‚˜°éãÕÓØàHI¢Š8½Ñ“>eŽNþÿóí[Ò!ô×ÜÖ̯@=·slÑЯ-ps“³‹[W—––&Õ€s>5æÍÒ%ÿ| L ±‚?¿ãîs‡·PE,ÿ‰06+xx˜zŒ–b~ á|ÛâˆdK²&C“.MªsÄ®Å~¶=ÝtªP¡&žãQê³äû»-5bÇ`â—x\¥[eZ©E¾â¹MJU†CÉ{|¼Å"þùýÁeGUùXhšë‹Nð§—viq;U îÂ¥>¢œÒ&Àæ–I{ÊNEëti`i`i ·<ó©1i—üóiÕ_^õnS6¾Që*b&8ë}7øïæ¨D&¢û8åF{[ÑL.Mªóæb;»-žIÌøx4é¶áű÷?à%qGïg¿ù š¨$Hyºä¥6YÈ¡Kþ†(w»rzg–ЋSøê!þ÷ÿî ۉعb3y‘9‰ÌåËY‰$†³ì#ØÎ^%³¹É• ZùKKKSkÀ9Ÿófé’£@^IÃy‚¯ƒÄ©>«ˆ ê‹_¹¢ ÁŸÉ1KÌJ$ì¿5ÑØúéquR#ö)n©R—¿Ûâ ï#ZØßm²`Zz ‡"ˆ²U€¡ÜѸ¦ œjy96IL´'Àÿô™&²Ì«ˆQš$!º@‹ØÏx ÐÎ]z@&Ái¼ô`©Àk¥LÚS®Uvݵ4°4°4pYþù´wKùþùgQœ’ð» QÍ*bè5MÃVž%¤µtõó;d€ß˜D”*{Ëšh²jéš9©Î›‹íï¶À6ð‚êà”.ª¤e{®¿Û‚ yO‹,üršmzmºÂU_7¼|z¶¦Dd 2Ey|O¯ænlNbVyÀá,ñ„i¶E"”pê†9DI8=}@Îêá˜éTàñFONs“óºh–––¦Ó€>ÍNšFfÕ|Êfõ!"÷çw¥ùTºåªŸ˜[À´¼¤c¦?f1ÜŠ §4%§Fƒ®‰ÆPN§K“ê±Ç{ƒqùbù<]&=@ÂÚHzì¿þn ª„-!L¬¶©–º³(#z^¦ƒÌÐÝ“«pæ}41ÔÞbÙÝØªˆ¯B°Ü…<’ý”Ä&_ï¦Ei%XðhñçWà…Ò&í)jºnYXXxDw€ÁB§ìÌÉ‚©^ê1jTEÌ|Ç| à”1ŸR¢ˆc“ê¼¹ØÎn«µ,9z}1i2 –Î<öbg· åå5­W©§ ªœ®ø]Ùò‹C<Ûè€ÃøBkLwØGú*bn’|úŒ„+—¤=–BÐW2“hìWàQ$ONÚ4úE³4°4°4ðûÔÀé|Êzœ+=9ÎùT^‘>™˜þJmQE,&p ³Ø¶¿v( Âñi§zNK’¿&C9.MªsÄöã.êN»­z(}àâåI@wA_ºŽYòéÇŽìì¶©å›SiYßÙ9]A’v(‘g|L>^:攈ɗx æ8Þ›Í¡í¨‚³¾YÙÌG–¢JbA“ö”(ÿJ, , , ŒÑ€=Ÿr•I“9”ð¼ãŒiç8çS¦æÄ8=Ùµ®"ެ¸‹™WsSÌ?& Áõlæ Áé‘&欉&ªbXbR7Ûî¶±cÊoîà­HÎn[Ûâ‚qÖÆðÖHƒ$Ú † äÓÝ``,z…gxèkd© ÷ܾí½lD•DŽÍM.r^‰¥¥¥¤{>åjœ@#& I3.³Æ t—¸<Ÿ†ÑÞþÛæ‚0“z¾E•ìQ¸îNí‚Ì«k¢1ÕÓåâ¤:Glû‘ªVYv·=‘WáôrÜñ—K‘,M\î¶vü¶•Kº2F†x‰DLgEÒn0r†ê`ïÑÇpŽSúl¡g"0:d᎗'í)+jqXXX¨Ò€=ŸòNMxÁœˆ‚Ä›$<ŒG"=éŠù4¦LazU¼T >Dæ‚Mœ–(• øRxI™95è¡Áᯄa;ÖDch²Ó¥IuÞ\l»Û;£?§¢ÛºÛ˜NM‡ ïå96lAºžœŸ´á÷Ç7rpÉR‘c±ê×t^ 44¬^i”#Iˆ fË8¤I ’JÉÞ"z†A]eÑʦÏ2i’I¹@q ”‚µ-;^‹››\“Z,&KKKwÓ€g>eþ"B÷Q²ÝŒÉ„Ó«>šô+Í(ú×Ð…"ó N b.IÔ 6`þÌ‹‚$¡ŽŸ>3E*QrLÁyM4¶æ{\TçˆmÒEyº­Ñ7Kþnë[:nØ^¥‹•B,±º‘'b0mô‚=•½K± [!m˜—ö4‹Ã ¯ÊÒÇ©Äqð9Ö>)ª`Ð94É‘ègžÍQˆí+ ÐI{Ju-&KKK~ ÔΧÿþßý “`‚xd> /°ÿõ'Í,øs4È“ Û’ü"f¢vW#³DO>ÜåËŠ#Åx7G̹…YµàráÞì´«âÖDc¨½Ó¥IuÞ\ìÚnkôÓÝ¥0˜NDWÐòKOçÑÕØ°Ë·']5àá/_ñ‚’¦ó–þÏÏ-ÀEn1ú¸$a¨á%A`°ö6ú¸èµƒ1¥kt2èK>˜/1ÐòÒÕ´y679»¸uui`i`i`R xæS&&ü3a4Þdâ%vshzz:Ÿ†=ü_^q­h²ó8låùü3ì¤d8gÞí{1Kœ¸SJ-™3G£È`Nf‰~M4%ÍôËŸTçˆÝ;yºmÚýéÓn{­q…TÊÌv0Œ8ùð4êù‹ê‰Ò]Ú †®NÓÄJ”ÊcݧÏ<þsJ‚Q˦ïq•J¡1m¡Ã F$HÞ.hÒžbWj]]XXh®c>e…Œëæô}G¿L%\µçÖÓù”±]sœürÈÁkT´ÞRÙþø¦·W¸«DÏ%è d%‘Cþ(‹x‹ô@rªü‡¿úKf=ƒùšhJšì—?©Î›‹mt[»Wž^=í¶——®Dç¿.`ø2ŸÇoOâSe¨a‹KK%¶8™¡ c@(qx<Ÿ'}Æ"ÖÎÞâC6dn³mnrvqëêÒÀÒÀÒÀ¤°çS®2ü†cû+³o‹œÎ¤"¸6Ÿ†Ïg¼¼ÊdV‚˜ÀùË+§1—!f‘”YOu)Ñ3ÁQAˆ™òä –û¥D¿&š’fúåOªsÄn Ÿìn{(ëþ¬ã¤»îN#eL\ë¶žLJIDO£ìñÒVžÒ# 2¤ïÄq/Ù ÄkIïKK6qó« MŒ`üêµv“ö»RëêÒÀÒÀÒ€S€Ic5?ebϧŒ¥ømr…~5‡âƒ%£8NÙDí|ÊÜÄÁühJr`*5ºhÒêdÒÉ´% ›¡9dAy:S¯‰æ ¶î“ê¼¹Øv·ý‘>ÈsŒZ»‰Oµ‘à˜¨í¶Î&WT-Ï­áøô™SçÍÉ(U!-á_5ž‹U48™-"ù…R÷6ÉÏðÿûç?È!L컚›œ]ܺº4°4°4p+ 0—ýëý?òË4gÃ9ç| ¢#4è—¹•x¹ã4süó)è—YɹA}$f:sA‚r³M€œa‡aödÛnÏ’)SðÞ H/­‰&ÕÆ˜ô¤:Gl»ÖjÏÙmù@Fè­/¯ê•»ÓØUÓ„¿Ûúe¦Ó!Qµ Š`Á…^i÷D?óZJ)DkCaѧ¼<gIŽ oKQŸ>ÛïäÖÊ㤇Œ ™ñKœŠ1iOqjc‘- , , Ø` f=”„ÃàÕ?þñ{öÏ|Š3äíë« Ø&VÆa€q:îÒþùT!¾oA gÚƒT™Ö™IÃ\ðòzºq4þg¡ƒ¹Œ²²z`Å–OtEMIè°1öšh²šìš9©Î›‹íé¶ôGœÀ@¸È´;ÝuXú»­¿¡õòi|•U‘®ñÔÏçqJÁZâ ÄŠg;ª*ŠÊp1ÎXÎ…¶Ç¥M9P.íÈ/kaª…ýò`s“K…Y饥¥Y4@tXxX;Æï\Îù”áWã0›‰qh@&°LÉtΧÑß"•ÊßBfIÃ#%ÞÕhw#!ñÀŸÃíYzm[f‡Þg?ÃØk¢Ù©zÀé¤:Gì§xƒK}ÓÈwvÛª¶¦îô»¸¹ NŸ&k€& 2?¿³eÔ…±Qy(æ—'e–ŸNw–0¸]¾$1$eH»Ÿ‘ØI¨újˆn%<ÎnÔûéâ*6˜ xˆsMvÖ^¡ÉN—&Õ9b§¦õ¸rüÝÖè¡ÙKþnë©Oâá%Ö÷}Z”`åôÍVshˆ×åØÊ»ËIOéˬ= ž­f ~âön©ä¤õ&/ [Ù)%ž´§¤UXéX0¸‡VÏjSgúÕ›q¥Rœó)ÓÁñÈΤʬšO讹òl4æ¸t °_—nÒ4„Ždgí5Ñ4Qo“IuÞ\lg·5ºgéRU·=m»¸ˆCD“ް ³ÅSÞë!@ZŽ”Ra´iÎ.Í †ž­f@›Œ¢éî1éà³ãÜõ”rµË±–À¨…]\s“³‹[WgÑÀ‚Á³´Ô’óA 0K2z3bó6+ç|ªØ`Í_éoi2%ß?ŸòæΙ8QÝ;i(é€÷»ùÛÄÜH`pT€Ö~1<-(¦õ_<‰5ÑDU KLªsÄþ}zƒµÃÚ kIþE§E1|ñ˜66|w8k ãv‚CD }i²ôo»ÇhHßî1Y>M2zß>š‰Y¨¢6 Ÿ´§4ÑÕbbh`Á`C9ëÒGÒÈÍ9íz`0.!ÞЯ¶{ÿmƒñµ*‚—á„ý.¶Þµ‰› [ÄÛäˆØaMs›%O§È¬ ,œUËS2'Ü›‹íé¶Fß4.ùŸ^ý`£¦ RñÉÔÏ$K‰à¬7â/9Gâ+·šaåfûw9–Û$G Œè]#~oƒys“3ÊZ—&ÒÀ‚Á5ÖuŒœóix_l{'Ž¡8=ŸOqÅÀ™ØohM³äŸA'©ÄÞÓ t­Ø8?ê´V± ×j¬ý¤“;b;Kªsv[Þçà1P¿<ÏV—zÀ`õ ü™,ßð(Ê K?ΚÚdè't9™õ+ïñ.m5f™¬j«Æ%JÃÓÝcŽå6ÉÙŠzœ·-jÒžÒD]‹‰¡ƒ å¬K¿O 8çS*GœFc˜Uó©FxÀ-Ó“µü©ãh×.€Û’™Ú*;h ïÐ}ù n·§È]»Sõ]&§k¢9ê¤wΤ:o.¶§Û;Á{1Öˆ}Hlw^g·­mèðå-!iÛ–œÃ¶–'ôøi8½1Œ'¹=ͩțz4é3IX¢ŠnØÓÝcRI¦â“Òõ]Kš’€ ›s“³‹[WgÑÀ‚Á³´Ô’s˜<ói˜A6Ì4ÊPÌëÕ:H?ƒÃÿòJÈ.^`pptäf±¨$¡tè9˜JA’™» aÞ×팧³^,(&–78ªâé‰I'wÄÎ>¬]Ö§Ým1øØ¶>{ü?Þm/Hîß°ÅϞǗdyæ=þA–:•öl5«Øvéî1Ç"ºæ¨iwJ!$ìXë]é“ö”]-Öis ,Ü\¥‹áì°çSM—̪¿bƒ;·6™O51ÎS`Øvt¤±Áú T¶ ØQpzb'Dƒ§ˆÓ,½‘¹`°¡œÁ—&Ü›‹mw[\¾oè÷ËWzà#¼ðµu^žéh¥žÛɼ·bõÛýec¥Hpêçý÷ÿR9 4•Ü)'8œ$‰÷ˆè×Þ¶ÍMÎ)ê"»¹ ¾y-ñÆkÀžOã\ɼ æ|û…^šÞ~#Á1Q5ŸâœÑ $ŽC½¡ &;Í%²¬S¨Dœæ‹yÌ 08ç ^MTѰĤ:GìèQl¢+»ÛÒõâýòuwÃBÂ,»;¬rªº­¿.é>‡<²Fã¿÷”’JÅ"œ·ÔJRKo}íÑn ‰ŸßÙà œo×úI{Ê5­»üX0د«Eù;Ñ€=ŸçJ&P»öHÙi>e" ‚ Ö8HÄ9¢Ô^´Ä÷*Ž‚I¤DòsÌ0YD½&K“}®MªóæbÛÝ–·º€Iü;©ÞEžc‡íƒÕû°äaoC²Aâ)Š«2¸ñ ªÝc£z×JRK_%°ŸXŸŠ3|“Æã5 G®Ž)Ûæ&—2_éy5°`ð¼m·$ï¤{>MçÊôÖUñº>%n¬‚Áñ­·8û”ê‚ß§<œ–(•ÏÜœº‹)’y„"J·T1_MIýò'Õ9bô ÓIÓþK:K=·ªÛž64Ч—LIèé5`¹3wÊ9ÐáÆ*’rÐ3ëJñjLÔJRK j› ÕèeémsïSMÉY/ËÖ1-zÒž’Va¥{h`ÁàZ]<§Ö€ãS‡qø·WÚͳé©>…9ó#s4Ó% vI¥šòâ‹Û̈dÀZÑ#?dDkW ~³ük™¯‰&«Æ®™“ê¼¹Øv·¥8uU=ªWÒË„?ÁTi?Ý¥ýÝÖÓÐl¡÷¹mGØ5¢ V§æÕZúo ¶ÿŸ½7hÕ¤Yî;»wâ|³f´Ñ˜¼ÐÎg;`˜…ñÂÞ\z1 ƒÖÂpg#´ÐB;1 #l0m<ÝËÄ\_dniÌ;¿¬ÿ9ÙÕU™Q‘õdÕód8ÏÉÊŠŠŒŒŠÈüWTTÖZ¶VIZé×-v©Ñ£(~5‚‰'Ý$8lóïnrvsqt  åJ…œ§iÀžOó© ”hêâu›.a%}¢ŽÍiú®…Îô¶Zk·kÑ¡¦H£ài,°ñ‚Ûbw}Eb¢YëäèšAuŽØ5“Þ§±M·ÕÛ£xMãVøiþ$‹Œe¿^úÂàbïä•ÅCû*ÕÙ¡æVIZé÷ug}–Úê“XBb°¾ª¼&›× ê)ó.Dù  >B«Ásh lΧš(…NÓ¼ÿå?iÓ¬Úx`ë9 ÆbŠŠ×JaŸéCÉf Ñë­¦³äEízµ2‰¦¦ÉãêÕyw±=n›ÀáÓ3MËaõŠ+¾–]¸X8 úÜÑHH [@Ñæô`é…Öbs ^¶HÒJ_ltG%Cc„§‰TÞêcw“Û!v÷Sè–ÿçö§üö-cö0|¨Žå˜¤Àëõ·»ÉÃ÷ Ü¡8.*µ`ùrªÏI†¬K¶ Ì»Mói†²¹PS¾9#!”™õj”ªgrL³ÆÛꄵ ú&æŒ!«8t„Õ9bov“ºœnK:ÞÊ£¶ìžµB“ÛúeÖ}k¦ï«ÌV…â ­™fŸ$œ¥•< ¹.õé°å\è5¥jõ”ZwTï4ûšmõ™½Ý{eÖ¾WÓ‡¶{I›?TcÁ\°ìŠGQ_³ ?|¤<ß=`QÌGKSöÌJÐ0‰h±ƒ?D¤áédNw¾ ªóîbÛn;÷J…dçå.ož-<'Ëåƒðí¦EZXt÷ë ìv4>PßɰUЯVi`$L¹ßî!‰µ#ìþv79»¹sŽúÍ>Û³³pÙŸ£–hE¸¤ÍÇÅ=AöÀÂQHk¿Æ sÔÀÂLÚ81kSÇüK0lÿA£Éz³•p:[“GTçˆí¹_ókÌvÛì•<¾yõÜ·U¶Øå66¬ ¹­Ä¿~θÓ+rø5€‡¼¼®KA›í¹­’$>=“©ÅF]ÿe:ˆ’ôNza3ÔSìN9Í~mÕ›5™½Ý8ÚW—´ù¾* nE Ø ©¿z'®ök / ,ÌwÈ\ìK±’‰Rák¦`â9 _¿@¥6›8œÎPäA‡Õyw±m·Í^)ÈÇ÷ãÈêgKy³Ób/]Rú›.1H•äd­÷Åïfj«“yú4ž¾V#÷ééÙ57IÂM1Ë«“m.Mã{èüj°çM½î&·Cæî§8Í>Û¿¿pÐlÕ]ÁÐÐÀ%mÞèoê¥Í…§ &ü2u.~Ù%ó°6ÔtXxô FeV e²ãéÏaüü3˜Df^6’„Ù5TÇQ%ó6ý&q8¡Éƒ ªsľK4X wî¡ßlõ_bIÀ[>¥ùÆùÌ h•EfpX•Éh¢×,óò#áë^«$œé Ñô·>½ØÊ•tŠñ–Å"ô4 Ú­ ê)v§6g«¹©7•»ÏVvGâ踤͡¨à¹Ð€=°p”á—,!”#Œ¡¦ûÀ¢Ä]^Ö#¬D™g” U„Yôh¾«¥Ò”ëK½¦ÈT™SR1(³*˜Ÿô ú^#†>œn¡ÀvÕyw±m·Í^‰³qÿ¨}„—š“£ÁZ{1¸iÅ¡ø2ší¶­†„âs:X»éh•>pŠ9vk»Ul=-r¿Ÿ†¦· 7ë0§ôŒHŠx[³([û-…çâM_ tfûŠÜVö|* òL+E¼Ã© ¥{žž‘ç ¶yÊÓ¤fÌJ"àã­Ò¿¾âZ£Ï‘: NPä'`ðC™î b×€Ù>õÚn›½2¹í„„yÌÁøL»'~>#÷Ž…ÚÒ¸ñé3áhE¤ó¡Û ${äOê¤T^s ´VI”IB V«õ²{»À~)ëãÓg†,å“ÈŠ")"[x¯BÓlELžÍƒò :;œ£œhÅЀ=Ÿ’óÀü¢_¥.~Q¨i`1$\šG~‹˜Öm€AÊäźŸN ›ëmr”Ss*o­ IÂ/›G-l%…y[5þëi! b‹ÊÅnw“[ð¿Ë®Óì×V½YÓdöÌAÐßEѨ¡KÚ¼Ñß8ÔKÎ…Ç©<Ó\lé{F'æ«ËDEr’Ù$HتH0àÓgR!ÖÚS$G¡00l† ÅÌÃé åthP#v_øät[ÌæÍpXA…&<Ðt•Ɉ`Ä`£ÐWˆ1g¾-Õ·—¼¦Ä¦$œsƒáÌî6ÿã)ßndPO±;å7ûMÜ» pš½,!‡bæ†aKGOÐÀ%mþ½EÎEÏ1+M[ƒÉ|×9°Ür ô PÒ’ž¥?…S4Ù­ã*‹3Ö aé`~a^KŸÐYát í°;¨Î»‹ít[2x’7íò;÷ÓEù ·UVm@¦ÛÌœmÛÅfš˜˜Srb<=ó®Ùëêjõ5‘2¼°)/¢‹Ì­LÀ½ t}{aù 8RÙº›œÝÜ9Gf¿0iÏ®ÇìÿþWÿËÉK ©@.ú9}V65pI›ßìuÜ®çÀ’“ 5}ä_c„ñ ,·Ë/F²lÿœKú³¤Ö ¦²)êátuÝudP#öfÔ±IeN·}»Wú~ "Í0BJFÊÎýð±W$­•99Ò—Ü Æ1†)4x€œâ%tçÅ46ò"6ñg‘Éí•´+Üõ:ö >ý9 Fo­0!•7C+·_úààÔÀ ³ƒ³wAvœœó)$Æá¼iÎ~œÞ¤^Í „M¡ÛpÐÒúátÇ™eó :ï.¶Ómyz‹_œ˜µçÛÉn›ïRõò)À©¨¬]å¦úVæ4—kCŒ4G%¯¬Û«sˆC:„ävVÕšC¯Ä ¹‹Öc@¯M1º›\¯ŽÜÂÇiö†y×5m F’[:ç¡KÚüŠ ž 8–4O³Ê-ÜÌ0d´iXRµî’½€x‹³Ž^Z?œn¡ðvÕ9bƒ :êÇé¶`<üÂ~'n Žp[!UÖi–ƒ6uÓjgùuÕÊœPOˆÀ·lD†žù–¢ÓŒiëFÅœ·eEe-ùj}nÇE¼3G˜M1õ[iN³_˜´g·Éì™Yô²‰-m=Y—´ù“uø>›ó ,LLl$ ̳ ï’dX¼LEÌÜÁÆÌ{ÐÒúátÅkqhå :ï.¶Çm™ýå¶$ ðÜ4áÿµ&­ÛL@¡6ýçnR61O‰Á9Á`úOªùô¹vŸsÑ¿æ$¬n·7Å»@7<Û"S‹_OŠrw“»½·spšý¦‘¯ šÌžh|äBÜ~5»s¸¤Íw×R0\kÀ9°|Ï žð°¦W~×ãI®iXÖ‚5Õa°8ä·zÙí»´~8]Ó5êB<¨Î»†²ö©Åé¶ ;œ˜oÙI×…ƒÜØkŸžúé`I€z_Ç‹g51OùÀõ¢ÄÎük †%éÍœ^”áÐJ¿Ò­sM=)ʃzŠ­F§Ù¯­z³æ ³·»Gûjà’6ßWEÁ­¨çÀB ‡É+ ¿Óúcù×^ÎXйÁê/3WžsS†³†J/ïL ONõzQQiª §3”sСAuÞ]l§Û¦¼ ï[µ{·åF@h³ïA¶4'srkoùÓéž´„[ZYŸKïhš í±å‰5¥jº›\­¡3ëfo˜wíЙ³Õ™{Wm]ÒæßÕ¼Wg›ž®æ¥ƒÆ )ùyëzx9s`ɯɬըYCõ@âÍWK˜n8…_6`ÿša® §Ëª8­0¨Î£ê¨%¿Ûbóëmí­¹æL·í¨"RȘM+½ÿg 8‡6®×~=}ÔSlíùÍ>Û³³0¨ÙÛêzoG/ióïí"Þ¥¿þ…Äàyd‰g¬Œ0¤ðÈl¼mÎXË 70O-‹ |]o³xlUÉ,£ïÍéˆ ]ÂéVú;¼bPwÛé¶8&kÞêŽuí¡Åšsܶ¯¡hݼ ™–>3Ÿ‡G€ÑCH«†Ó5yhå :ï.ö¦Û*³]¾¹þ5–Œ8Èmµ :˜?æ…ü¼ÏKsT—–fxŒÅ%Íì¸C99Ì]9¢’ŒØê»y,ê¶»É[9¹rÓì×Ó³fD³?YùßÜ%mþñÕ~ K Dü¸.DšAžžd4­¬G›¾‹rÿ˜°hÈÊJž„v©d×¾ ó¸îßüãï¡g [ŸBÎ0iL D“(žI Sd8ÝZG× ªsÄ6 i‡Ò6Ý–U,YnKAVÜv‡k‡UM_·Ýѵ§à¶Ü·ò ‡ûV|öÏÿìOè&þ¾æF(×ÆÇ•üNÖ4›5'ÃàœŠLOÁ¬ÀLïl‹ÔSlÍoš}ͪ7ëÌ~ö82I»Ø]t`qt±» Ž]§¾½Ì§õ|Ò%m>÷. ÇiÀ9°…`àåi#È“¬ì’ ‘ÕMëzœiXÝKñç§g¦ƒ×Õ*¦õ4ËÛg3?槇Áìé™Qˆj¦+bf„ÓÙš?âè :ï.¶ÓmAM,2÷Í„?}JÍ+çå¾n{„ ¬y¦ç8OÏø”„·ÍÀ·@æÂš§]s2 Vs ¼ŒÀ\;†_®£$,âu79[!çušýÜže¿ÙclÄa¸Ï"Ãa×è{q ›Ó'T&@ˆ ä Ùò“‘9Ã&âù‰Î2w 4M+vÚ¡“[,Yû·éê™È‚ì’6¿ècì¡çÀ¢ÏQ ñ¦Ig‚¾ù—Ùv=ÚøO¿˜°ˆÞð§¿lŠîÚ§#$é¸'’×’"Ä´Ÿa0=â\ƒy8¡œƒ ªsÄÆ\;êÄé¶Ø035öÙ³qKÈ}«ýÆ\_·íØeƒÀž„­'– úÛŸœóД“¦ÓƒkZìΠžRìK®ô›ýz>²küfŸf½„¡`ÎMÄttïkˆùØæ85)|ÊâÃ(PY„ˆMÄYá΃ÌkO'…°kœÈí›A0?$\ ´F3ÜýÑÇt·[éã%m~®(¤çÀòšjøiy ¹0 Lk¹'VÿÀ²»k)7xk5{‚B978…ÂÌQ‹É%?H%ð’C.E ÃéŠj9´rPwÛé¶Lˆizúqc61 Á nÛÝB¸Å`¢$Åfçówhz ÐÑbmišM”XÐ"£ È·TFPº»É•„:»Îiö†y×9ÍŒŠ7ñ ]®“KM ~b(…~aŽ%kW©/5æÔ§Iùé“`2W¡ƒ›ˆFׇ¸Î<‹6ØXž‘] µ?bMÌ­ÀZ .°Ÿ­QRBà–F°éK@)àöûjÚÿ%mÞPNê¥çÀÂ(Џˆmœ˾Žh `,Â)üÐ3P8é_2ˆÃé åthP#v†.]4ãq[“`3HÊ#š°n)×ÀÀÑnÛ¥ïk&¯Á(úèF­O·kšt6«îG–ÜtÈ9[îpÜ Çí$¿Šóà °§G àü3l‚üe(rÝÙ…¸[„ÁMÄF£ëC2Âü¦òØóîœ?÷¦~Mñ4…§Ð§n改œÒºžž™‹?°9Ä8FM¦É…Ag‡,î¥Ï|Ú4ždbçÀÒÚq\FKáPi±£oi¡$ãa*{™ñ ñ•÷f½”Ý4(œÃé åthPwÛé¶B¿àa&G6|3….÷Š+S$3&¨@‹‰e—_Û!áb=i…†BÍÇ9*&¨Ë [7whMÀà<ÝÜXðÏV÷•Ø~iÆ%vc´`B`3<ó&K6˜c™9»@¯½@ >,Fb›ˆFׇ¨kæXæšu%}úü'¿üK )f^^«f¨Ož†¨¯…º³5©¢þbpΧ;ÿÀÒ¤R½”Ç# A€aût'-§Zþö’ÇŠÚ)pcXÃõp¨tÇj¾ŽNWSãqõƒê±kC÷>]yÜô‹³q'(L˜Hña 5>Èm÷uÓs–bhPòþýÒ¤\ F‰›^s ›h@ú)bæ}:Àirß×<õ”uGæ5³¯¶]ï4{Á¶>™PHùúå׿«®4ÒD<ï¦Ê «®ës AQÜ™Û:nÓ(Îó¡u¡‰x}º]ƒ1i “S -ƒUsEžã˜÷p[œ”¨Úà|I›7ú‡ziàîKkGð‹|{È“ v{I˜38à˜äÕ @_kQsJJ=úô‡â¶Ú †I8]M“ÇÕªóîb;Ý6=R|K®þ1±Ë¼c@'8î·rØ ‘RkêÊ‹1AkÎjKÁ%nu?}.Þ¡ì е #½ú^dÒÝ䊭œ\é4{ük‡œf©¤£iYæf¶ššˆ×L 6QŸ¦ªdÓSç·ZÖ¬¨i".r¨UÒM½€£y™°¶©ˆŸ 7(ü±Æ™z4€†¹‘Oªiélƒø’6oô7õÒÀÝgGRÄæë<_È Âè%æƒ ”æýß§')öÊšShÀ€ZàI[óp:CíTçˆùuÔ‰Óm™˜˜GÒ,ùá#·‚ÁÌA50@½tìËí¬ébº$žž™à§± h—Bñ rM¤Í]íăê…ù‹Ìõ”b_r¥Óì ó®ò›=ŽŒÙh#TbÇ`›ˆéæâÁÄæ !c–/ÎÍJË…&â|–¿€›2À 1²Óe$¦Íl¾n Ýw€®‹ùÀóS.ióóFù <ÂÀâéù é¾ûë0*€ô@Š;D&wãt¼@ÐX4öWçs fÕ«¬8&»ƒ õžhЮ»Ø~·*%_P1AÎP½œõk-2«j®d¢$¾”ý½F¿»~3@·›ó¾Ó{å™rw“Û'aß³üfoùú¨ßì™SH)gêa£`?޲q·%[Nå­©žÌ;šÝ(°[£¤¾éµ—&b£Ñ⡬^)cÄø+e/¾ÙæeÁfµb0¾¤ÍýC½4àX ÃÂÁŸl7Å Í¿î&g¶vÒA§Ùæ];ä4{ŒƒaÞQ"zš€êë}‰Xoª’§˜ ¿†²xÖI†l™€6g7ájÏk/´ØDlH¸>¤næI6‘ê:át޲{ñ>P.ejŠ0˜z¼ÄûD¶ ×\Òæsï¢pœœ SõhUÃÈb·8¶8–»¦Ç%6üT™úNaát¶Ú8:¨ÎÛ¾¥jÕ•Óm™¬Ó¤óôÌÜ:ߊ«Êsܶµ¿6=}ô„ÅHŠNÚøðU襞”J±ƒS²ñÓ3ôÎ¥ilQ»5R‚3ÿA=%Ë_,8ÍÞ0ïÚ!§Ù Ê"¹7‚mØO ÙŠX ÷'#bcžœs3–¡@ð!– `†§ýÚK+qQùµJu3M²Óߺ‹‘¨ÌcYy:Á³ð¯â+~C%Ù\l”Üæ»—´ùy£|î>°Ô¯ïl¿~!’ä¶´&p§¿pºNŠl`3¨Î»‹ít[‚KLÜ´Öfÿu½4\³ãI‘.n™™p!&HN|‰¡üÀük (ŠF‘.€}JÇ£€ŠùsaqÞü¾dÝM®c§v³ršýÚª7kœfÁ`Z¾ƒ Öcú²1O8ÈJJ!àÙè»ÞÑæ¶‹P?6s欽ÐfŽÁÚ¯½4×Z¬Õ«›9ó0­f¾Só¹úú…/(— w3õ\qhtŠð¶A|I›7ú‡ziÀ9°`Û¸'›ÆFʸ¶1¼8–ÖŽàÑZÍž¦-åÛÏyjAEò¸ˆ1¦×ëêMÌÃéêŠ<êÈ :GìÚ\¹OSN·e>bÃy³Ÿ5Íåbá ·Ý×MçYMa1|œWP‹ývmjz×Ò4N™d\#DEf:Ð=åQòæÓa˜ê)¶Zœf_4l»Òoöé¡ÀäV0 ódÁ™&“Ž^9á×€µb’žÄ??}Æ8•ÑTc.L˜Á'ËñÚKq­E£^:Á2•eÄ®A úå&Uè‹!%‚Œ¬Ñp›`ß´^Òæ ýÄ¡^p,Œø>6©Á„³òPP^üKS_yhš‰Œ—õ‡Í8]¤p¥rqSɹŒ µSš˜‡ÓÕÔx\ý :ï.¶ÓmÓü;¹-wó­æ³Ôä¶Ç™‘>zÂbÜ5kþ%(ÞH!¦)$U”ûƘ7-MSduK%W„ÞqíFé5=Ã|:L‹ÝMî–^ô:×iö†y×ùÍ^€ÇC|ð^ñQþ¼¿Ðciü’½€?ÎË"æ— ]¤¡†XB6û™ê›ˆmVÅ£ðàG` ìÉT‰=cÉKÐ>cîíäz«®þ¿¤ÍW{úiÀ9°$Ì]êÛÂû Ïv‹c‹`iê S~s½z@Ù8]~Ä\¦T.͘Æ3Í&æát†æ:4¨ÎÛžZÕåtÛô©ˆ)rµø-:¬*rÛÖúés%z^ΰo¼Ò¨5iƒx›r¥jˆ†—×öÝKÓ¼5Õí?( Ä´ÐkS²"uÙncPO±;å4{ük‡̞ǽý1¡¬SVÞ¦ÿDï¡É5ìærµ0B.tZGwÖV‘>¿ï¹ùÊ$§7›³*Ýbƒæ‰I?ß^¸Ñ[¿"‡ÀÕ?@}S!—´yKçq¬“œ “†9Ép±[[––¾0/0Íé½ s‡*²ÁéÒÁËÒK æÊMÌÃéŠ ?´rPwÛé¶8&¹@Ĭ´©ÌoÑaUyÛg̕ΰ˜Ò¢˜U7´/kld£Mþ¸®9óxË~îÌ)ÝMn-Æù5~³7,¼xÈoö„娀7¶Í$mn²È fbS€­4R áÏ=ÙظֵS I·uÓ"Ÿr‚D‘¾‰¸ÈÁ¨l›ÕÕ€÷`rî P 8ÝlVVŠÀ s»Ä¯PHÞ].ióënFMw <ÂÀÒ½Ss†ÌzÌ\xÐ0l/gÃäšÄ¦S)ópÊ‹@f¡w*yÓ°xCJÓ)9dz?”˜3»+µ}¯¸¤Íï^”ÓÀ# ,‡u.1f¾Ë·“ŒÜÈ÷j.œ®—&ý|Õ9bç ËßYƒrÓmõš9³Fq3Pâ=ì!!’»˜Ö‰¶¥GEuðÀ´;F}ïÔXÏê{Íñ%=ÔVúqEäÏÃZ±ñA=¥Ø—\¹iö†aÛ‡œfs; x“Hf çåA W*]¬Él拲 ,ô¼Ð<Ðcœ6 ÎF¨¥Ûn'¦]0*Ÿ-á#Dâ$áQŸVç^ý5‰ÍÙ˜.(6,ŒLáÀ6g ô•ëqz¶=ž“-Ê—´ùEc÷ Ü}`9¢SsžøQrÞo/`` ìÎÞR§»E{ûÎTçÝÅÞt[0^ UMé@ò¦‘äÉ×Pì»|w?KÈD¿Ì¼Êí¬À€faâcLÊlÜS¿®W6}¶àÌî€2ú™ ½0ènrF[§Ú4ûšUoÖ;ÍžIäuB™úŒåpKbtŸ 'ôJŸHXΜ}Ò=Úô2¦ΉÞ\Yæ`rY&…MI<Ä ß"ͽ0λ¹Ð*6Jêëô´¨r©Z² ~ã†àplžp±­ÀKÚ|ÖsŽÓ€=°h}`¢"Œÿü®7cxq,ÇuMœ9q~q²I˜pº&uu!Tçˆ]›\ö©ÅvÛì•Ãy—3,hj^³(?ˆÛîSËæY‚1 æSÖOc¶×}eê'«3ÝMäíÓg´Ê¤|æŸÄþnBfœP‚ ê)¶Vf¿0iÏ®Óì¹XÂ\·ß/JIt…Rrløç–¨¾×aoB›Ðƒ)0gv“eR°m²‰Øh´x¨IlÍÈÜZòd7ÌI#sÎT®ûžî^K˜9Ÿ8¿.¹2 ¡M ؋֙ÇVk›1Â8–M o$HI€î/N6µNפ®.ă꼻ضÛ^¹yèAܶ‹µ™dX«ÅÐ‘Éæ•@MôüÚ°g~VÇ2QGFàô¦ÒÏ?|«ÅÐæ-v7¹9ó{•Áìe\¶m=üøfÙÚ9x™O«Ó0©Ç,¡ñëR‰ Bïx¢,½nÎîæ%m~­„¨é®{`áè+V0$ãá·Øˆ1«>Â|Êx‚üÙ×ì/N¶ê6œ®Uc·ÓªsĶðVÍØnkxåæ¡GpÛVm4Ñ 0³tÚ Ð51?Ž8ÇД'Y|ŸhÞú ž2ïºüf?¥ø*Ϧ †¹Xéæ…Õn§Í&¦¿(LJ¤t“9Ä<ŸEˆÓ›ž?Bî…öšˆççr"OOæ5ë2bØËµ-NÁ•ý«;»ÅÑõ.nb„YÍ5—´ùÜ»(§{`É_Õ¢Ázï†5Ǵ˯1«>Â|ª$Àì¡x›z)3œ®—&ý|Õyw±m·5¼róÐ#¸­ßvS2ŸjJ ñ‡œZtkóY¹ºÓÝäAK`ö,{Ë­×BÒ*¸õ?(yè  HlÃFÌSèiZ¢Á¦|B‰=(ÝÂ~û»‰VpÚ±# )K[Í«,e~ó¹EÅpô5Á~ŠªÙ}qR‘·éE‘§*/ióFãP/ Ø‹`0F®–(`ŠTnN¦<Â| îe”ÈÑà^JŸpº¾úôpTçˆ)z:褱ÝÖãž5šGp[§v’µDóv6qØiΰíê)¶æînö 2fÀ¼ÐV1PŸ:è—”rÎ%Í»ÖG1ç +EðVNzTñá£Á|-‰ÁÜCŒu)â qz‹mŠ`S`·&súNåPÓ¯>äñô¼ ȧ§É¨qS' ½?=£gt˜Ð¾©KÚ|MóQßQöÀÂQ ô9MšØwTÔšU8ÝZ'G× ªóîbÛn[ƒ¸žúËÃ`ܜѼ£íÙÏ8¤ÏyÂb°ínr~Q£¼»Ù+I;¯Ñ±Ø]w|¾\§–%1^dÓ³K˜°².0˜gfÛÜVfž€ñ×/:šL«­yÒDœVôýð1…'˜šãØTæÖÄ`·óî­ÊZÏÍ,(µ‹jŽž7­û‚”<<½ýø'cN°(_Òæ}ŒÝ#4°9°à9¶ÊFÁø ëbªí>Ÿ"*Ž@ø7ozJh«wcTáþ]÷ŵûñÌÃélÍqtP#v-â±OK›n»pFÿnw·Ý×ÁƒÎÒÄêæ$F+[ÅÐüa1øê)¶fÁìy‚À{ÖZ©ƒ‚íפ@@ÀF TlwJx’±fÛâz ð™GkUN1ÛÒ¢ÄMÄEÙ‰÷kЇæ•i±Qe`-™Æs•S:ñUKPFrê±íbF±(¹îРºcü]ÒæþÆ¡^p,ðù Då6SI}ú™ã&¶*”ïDæN'Ÿ*Òï`NWÔä¡•ƒê¼»ØN·õ£ßLymŒqjpFó5f?s0bûÃbpînr~i£¼£ÙƒÓ˜]é­+ç4̃œóòœì{™ ‡·?ˆI¥xÛ+üO ß^Ô¿6Dl"¦1¬ÂM¤"l‹]îg~×G0f /h3x.s2ˆSצž®¹Ík.ióóFù 8-•¦dþ›gÏu¡û| C¶¹r¢Ñ¼rQæ†Wâ–Y>¥Õ »;˜‡Ó­ÕxtÍ :Gì<àwQ‘Óm×^¹YÓÝm»ô·#“¦h^Çvod¥Q—ëŸÍ°4ƒzŠ­¥;š=ŽÃ%È"UHß11ã0üôÓ‹:ÅlÅ¿vyXIº¬­òf zš†^+J]¼8³8±‰XY¸zQŽyz²àæÙ-"ÛÚ‰1÷ôŽž²!Xõ—´y£¿q¨—œ‹bªxDÚ´.Êôk̪ÝçS}vÞq–ÍÇLsz£¼ƒy8¡Ïƒ ªóîb;ÝÖpÏÚ¡în{%´²ÝÍkmå ú| ÿñ{¯‘±;š=s w¤¤ðmÝà”ú[Á¼‡B ˜£`H€0µFœÍÞz²å×¦× A6dƒØÛ68IWP´–$›s~Q0í‚’Ýñ+ Ÿr°yø‹`F<¡û0»–3j.©çÀÂäˆ/.’] ­6“ª¾û|*û_xúbw÷5ÚÁ<œn·¶wŸ8¨ÎÛ½whÃé¶¶‡vwÛ½;â:ËÖÍ;B’›x~{Qd,}5ÌüÔSÌ>ýüf)QqêâãEf%[ŠM˜ù5pÄ[rÌœ{Mò4L² [áÏ|h]˜gÞ Fò»&S“Éé2ðfœNÔ#W~kœkõÜxnö7Ÿ[#FhÃ^A.3¹¤ÍçÞEá8 8ì™Í¹bðA0XJ`H=­CÝŽÊibN×QóNVƒê¼»ØN·-]»òª0¸5šç4ÈÓȈ%ã…_"H £õî&g´uÚ¡»›½0!å§Ì·^Ó^wŸ×±™.‰¦8"d°d#=o æâŠ¡ÚZ3Ï5p“H’"Ïõõ8ÅCÌZgšOa¥×HáLaSì,Ò¼P ðÎir¹Hœ±±`°vùÍg- —´ùEc÷ 8|“U`0NÁ†ÓáÝw™OqIîÙŒUbv+ª‰y8Ýn=ï>qP#6óàî^¯Otº­í¡Å£W…ÁY‡øøf4/?N±µŠ;h p®úCìA=ÅVø#˜½ÞËVÞB ù–¼LŽªç—²:µ¹ò˜p/Zô\_;忀$#­!\ÿó3 @ÆXQP)°ÕW‘m:#Þ9ÁëÀ×/Z]-e•˜Pÿ’6?WH”Ò€`Áånò n‹Óh®eÍp"&2Þzã·¸JÌn]51§Û­çÝ'ªóîbûÝ6û£³pÛî¾â}OtFóú6z;7 ¯]ñ[ 󆺛ܜù½Ê`öÄx™w”‘›Âò¥eʸRB°¤L¼¦s×3(²2S6ÂÓóüÂÙ÷ÎY „!s (Ifî!Flõkþ›8ï‚ÁEd›åYf.iÜ+ÔÁð¥œÌʼnóݹêæõQ Øp,¬Õ#ô‹­¦mºCäžÑ˜[˜OÁ[s§~ý»í% 3±]heNgë󈣃ê±í­UWN·5ܳvè·míÝ¡ôžhÞ¡ì`.¬W®ˆ?0ü‚ l>ƒzŠÝ©4{¢1ÿ´¢QC¦-6NñHÒDœÅ(æ?磹™«f3šOT ¾¨a—NñZ"/òdS9—´ùµN¢¦»<‹2{y{”slÊ䧆o>ž9Ÿê9 Y¼‡ËÍ&¹Á›Ó_c­ÌÃéüºíE9¨Î»‹íqÛšcÚõ—‡Ážh^/síÈ'¡÷éKdy½›yw“³›;ç¨möt™{ndá›i{sGð›½À³^·œ¿z¶VB1§3µÙëžÍ›€92(u»ó£‹rñâÜÍÝóÍ{N,&Wo¶X$¸¤Í{•}5`,y Pø7ïRà Åù0˜¾ëéÏJ@2tTHópºŽšw²T爽9#85 2§ÛÎÖYöã&–ØC{á±DÕvÊ›ºÛ=…´¢Ð}É`›=G5Oa%Œ‡=–ï7{6jIGŸžÉ| -ú‰ ì°¨Yy$j5~õášþ%·Eiµ´§çôu•ú_qMùHs. ÷l@w=Ú(3m¯½ÝæÛÛŒ3® {`Ɉ†îUU£%ïƒQ:³Ó¿<(±_"Øq…üÌÃév¨÷ÆSÕyw±n›ý×_ðã/å#œ€I`c?n­{°Ûä±ÌHÆä[3¿Wmöt9'ìÍñ°¦-;8ì4{.œõ³’UæIzsÍ4§O +ápŠùça”8g8/‹9OfUÉm‚Õ,¤‰xÞŠ§ÜÊ\—†Þ1w§u˜K)Á?´;}YV~ÿáÐ;»mþG6±÷î4`,yöÌŸÏ ˜[àdÌŸ>g‚uÁ9°4k§øöú]ÎeЮr€ìëî²íçV?œîg~Ñw±ÐÆãí :Ð!¶e¨ízvºíÚ+7kŽrÛö>tЂ'Úb¾ùòþA2ì`ûýqù·Æ´àÉœwx ß;c- ¡ßü; ƶ¨<ÑKr‚‘¯ˆkú&AHÃøf/Ü«—¿RÀäoþ‚&j®ÝD ¶ûoþ ‡*³aNª^¾¦ó‚Rø2—HywNI¹‰xqîæn+s:E†Y0·ßî!àØÐŒ®fM!°Úaó›½ ‚÷ ç|šîµøôr\2ȧgnEoXZ•Ì3#döœDâN÷~ºµü¾ûcÉÏœóÂé~TÞ{ƒê¼»ØN·5ܳvȉθØ]ÛÐí°"W -<.Óµc˜}ý@b4#‘’_¶ŽŸÏ;u១ï¼0 ÎVÍ„5‡E*]œf@…Êç—¤ "Bˆj—¼‰XL¸ yvc_ZI‘¿˜ƒuC¤ç³]ˆ‹Í•M’È3”Å+1fƒ¹ò¹:é¦`úxŸAÜ}˜5ÚŠCWÒ€>e`a}˜¼Ù˜qÆ9°ø•‰Ëó:#ŽC¨“‚éEÂø bÀ¯9]+s¸…Óù¯]/ÊAuŽØµjŸfün»žñíšîn»¯ƒÝÏÒó¬þ=³ê¸6Æ“îmõeHâeZ#ýÃG=D~]«'T뛞‚Aj”žƒÞuy8Ì<&d<¬SÑÛëÖéÙ%Ñf.D M[mBÑUh"æ¦×œçnÍerÕ/øœH™ÉN›ˆ‹ŒJ?s…޹4J F{œkpFZ&š@®ÑoÚ|íĨçpΧ[pL XCÍÉó)éOLLgDw)“VÄpdŒBt£J(Ò/°¹æt­Ì1›pºó}gPwÛé¶¶‡^ó~+2%ì÷û´ “6jjÂù¶]k!‘–q‹.šúí¯Û¾±2L®˜ü°Àª#*3á+T”¶§gæ à%SXÑàUÙdöh^Œëb#U®W+q6dfVÒn1Ú£9‹0)4ÌŒ¶=4gœ…&æô‹‹ÂÜM±)8[Ù$3l~óÜ xÏpΧJÑyXÞî¯sz ,þKÀx•§ÝZæ', &1FéEZý2Ò…Ù|×Ïœ³Âéæª;§<¨ÎÛž¤Zµçt[Ã=k‡šð@«ØF?Pnð|h"Ô¹‰¾ÖžBú(•µä^Ž.hlŒUÃ5­eQÐÃ?¢”Xiß_ÛìÓ€ÿ67eô»ùÔRbûÍž\ˆüx{f×¶jžûCC(»oPÞïº&å”Þãûós{…œz~šy‡ÊÍFèÝd+"öÅéßõðíå{yAÔ¾‹ÕµŸg„~¶ ÜG¿>]šî¬˜|ËfÐ;y>gOÙo(Ð5·ù5&)†Á~æ° §›ëöœò :ï.¶ÇmkŽi×ûñÀ9WüÐV¸ƒæ¡Ò¡MôbΛS< 7çr69`WœËZ‹÷ª‹Ê “Ì¡2'rHhV#*5ÌilÎ"€Ôå¨Ð/úØŠ_ã,¹ƒßì™AôLph<^Ôu¡é„̵ćìÖ®—Q¯hϼýýí…7Y[Yîé·öç&FH«„C‚êZ‘XS·ê_»©žúº äVÜeÚÌýG³ÍûO ÊÐ`è°§EM¾üá#C±vU’kŸþù æ¡Qf“×®c'§ÎŽÆ ÏS˜}sˆÃé MthP#vÇ ºuº­Çµ4~š-«¦hÞÑò8ùÏí'• Ø3qÄä<É\n€âœ9gSÙ»vde›=³±_!^žT’‘7—±ÁüfŸ¦Âé]-ž0¦$áúãEÌŒ£`rePpÃ…TŸÇš,×dO33ŒÛÈ–£Ð“â«Ómb¯r"&„}öÍ ÎY› B4@D®5œôNÖ„ŠoÚHó3¹ér˜y³¸<íæô]¥ðió5rj¹ÁÒ³˜ˆÆ`ÞDŒØÿ«×shÝ6ƒ³tB’— Û’èh6`Ð…jvªHdÙæ›Î âЀ=ŸrTHí×”šOáÊÇЃví‹ØDßDNgkþˆ£ƒê¼»Ø¶Û^¹yè ·=Âvðº0”9£y;øw bƒ4À]¼‚¤h§ÝÖÚä<9B2üf¬+ø”1-l:“09iúÞâýrš½f+LˆÅFîPy; F6½g­¼YámC`…FH˜‚‘Âg0YGƒE ÎG!*'“0?ÇÖDŒœtPKm§—¹n@O 1$#%IR§&°†fLã,£ïM‡Ö6ßtz¿[ Ø v•Ðf}3fÕ#æÓ<‹i¸f:ÛÒ]y寉¾‰˜Ãé*Z?°zP#v_¨`»­á•›‡ŽpÛ ¢‘µfag4¯‘÷!ä@_ŨÀ )0½¡l·Wóìà*g k€ŽvCguš½ôCLl Î/puÁ<ÁgÞQºÖþ24@>0)ÄzŸ‘ù ;þjÑ`$Éé \„1˜·K‡2¶M±ažÒ¤Ÿž¹3“ó`H”г¡ Û n:T³ù&&Aü5°9°èƒõBÕ&Ö#æÓü0EiH\/œˆÊÚ…k¢o"¦ÅpºšÚ«TçÝÅÞtÛšWnÖá¶ÇÙÃÎižÞœ%Þ•Pe¿Yx‡0›§Ñª!Àô€9HªÜ{Óäˆ12†Ï¿E< ͦ§8Í^騈NaÒÀÃÆ„ÕjöÄL~Ù„ˆ;”“™ë\®ˆÑæÃÝn´9ws}³L\åùõ f¦¤b úS‹§pžPгAÉéÐÀYl)ó ¡ÈsGå¦Íïà§¼ 8Fcî¬wÙ¶ñ4‚9ÈÃøln†MôMÄ0§ó\²¾4ƒê±±®Žªpºí&è]´âŽ:‡ó¯?šwŽHF+ˆºZ醽þÜYÜüžBbÄE Låˆ0«NHxZ¦ ô›`ð§Ï„(×ÖžküfÆã¹?÷P<ÓOk±YýJfÉúñå‘sÿ葃9KÖ-ûÜ*‘c~E]óš\Fi¹`V/ùú…»ùкðšåþíE+*3¡¯i5Prœm€í·ùÿØ}çpΧ &˜ëbÃYò0².ø–¦Kßw EÀ0±ûô&ú&âp:[óGTçÝÅvºíÚ+7krÛ#ŒážÌ§šR× ó¶§»‰‹ZM†ØW‡…Áô… Ð×F¿ÙüfŸr_?|$8Ÿ&‹§çä³±¡L&) œ6Åa?»Tô#ÿÖlžúÝfoäÁ¢–”¶7½iB<ŸÍ©(À¤¦-õ-¹MoÁp.J—‡÷aÒt–™'eN´OÇ´ü‚´Ñ…Z[ê‹9ÍÖ;;—´ùšr¢¾£œ‹î¯yÃT¿X¾1ªÜ2°Ø½#ü˰ÆðÂÆtFv–Ta¦(ž¨)‘À¬r‘L•MÌÃé MthP#¶=5´ª«Émm?]ÝZ»pz®‚æV&bçR3w‘sݨ†/†>1¢m›iPOYw|^ã4û×gôo,SÈqÚ¦>ßõ›=A• hÉKáŠÌ%\”¹XjËÆT•ndÌ?Þ»OB¦©Ê^Ï¡I³Ù²lò×/ÜN¢À”º0¥Lo²Cr´fØæø¹zâ·ÑÛ ¹¤ÍWuúiÀ9°à˜X¬|3ÿ‚‡ç#É¢ìXšzCëLaÚÈpP¡ö° ÎÐ3*R`삘]£¹&æát†&:4¨Î»‹ítÛ…Kzvrۃ졉-ƒØ‡ð™éµ‰É½ˆAïŒrÜÎC£ °%énrvsçõ›=¡þõãKÃþ7Í>‡s™#xª¨]­aô]—Œ«Æ¦Ù'CèõY‚9b3Õ2gq.»ù±L¿O’|zßï¦é) …ÂóºœÑ ¸WôÄy=ö\£Ça¡fsë}Q!ùÜKÚ|î]ŽÓ€s`!L*ÓÅnÙÒÍõdù· ,û:ôÅkòP°É„ñ'{!;7¸‰y8ݦò» ªsÄÎFØE'N·5ܳvht‘ÿ.LP/Oñ¸™‰U“,ãØ]„imô5ôçþöÖ žb«Åiö``®¬žZ?dc·fðªß4{¸Ø”yKA^ ¶ý`l¹GÊl©=µ„LÌõ'ÃEm5¶}’d1ú”, æi…Ê)o®¾‚Ò Ñ;ªtbãeOŽr_À/Ïnt›`(ð’6ß÷z·¢œ vȆõjÐH–Ézà>bxµáes`)ʳY‰/(Áž0víS¸3e ´iòÑ&æátYo§Õyw±n[óM£þ ·=ÍBŒ†PZ~>þ½0 f°Ã'Ü¥Gü[bw79C±§rš=¾ª¦-ýÞøì’¹p.…võ+ÈgtŸ¦·L+l$<ÔVŠádîw8 üŒÀ`Å5ó}’¬ùt¬³Â° Ì×îúuJÁ`íò[FGu_€™ÊÉú.Rªò’6oô7õÒ€s`ÑH’爫¥hQÞíM©ͧ ¯±èé»<ìÖTÁHÂF›G*Œ-Úð¦=õ~æ‡Óš<èР:GìÚ¼°OQN·­ù¦QÛîëfß³èšz˜ár°é‹}[9ˆ[?}æA917ä§`74¨§Øò˜=¡`ÍML¯Ï.§ˆ ì>[ ¡2Ó(€œÐ±`9‚Äz}Òú¶2Û&}æ¶)I¦<¢@(˜¯„ˆ3`•^×Zy•sJ'Ö"Æ­ˆÓíÃô”YãLý%mÞèoê¥ÏÀ¤™ß½ÕHòz¯=}V¦6¥1Ÿbñ²ã¸ýD ìÖmz©DÉü2N°ÙHlbN×Ë›ø :ÐuÛé¶5ß4êpÛ¦K|1БHÚœÿ}ÁÃ\’Í2C.ÃÈ©xfŸÒÝäìæÎ9ê4{&6€™ì`¬‰à\Y X á’åèq ›_ænÍòšlivÛ¢ÏjO–lJ’)(p_Ʀ?¦]cñhSø«®X¸äB¼×û‚OŸ Å“²Èe]Ìw/ióóFù 8¹0Fˆ¥a½iœyzÎãLqV=b>ÍÓ–'³H³çs–¨·‰9ôátÙ¤ÁvP#6Öhô«õÓm‹ŽiWá¶­½;>Ý;ýÂS-{b=AOšxžÅËYÇ´6Ž}â žbwÊcö€^mò³Kj„»ºÃ`¤mòk nÓŸ‡KNÆ\—›akj7­ù°þÛZ¯LgèQËúì\C§´ÀZžÅP¶ÈQh„±)ÛA.ióYoQ8NÎ…±JîòR˜F)|þ|Š#0m1y‘çÀ˜æYFŒäR MßÊ<œî8³¬qTçÝÅö¸­ížµ£ïsEIô!°š±=N=Coʧæô`+•Ͱ’w7¹GІmöÌ<‘gªbÌG?@_’ÜØxÈ.Ùq5›§¾Á쿽è~¬¨D;C3#Œ ™®Æ³H1Ùñ&‹–bK+‰±ëÏ?£4T¨5‚µüЧ—û´ú™~¿½¡ÁGŸ×Ä©¦E'e¾Ú$Uýï’6_ïné¦{`a|À‘ñMe[Q˜oÆÍuÛÀÒÒ­šÂ°  óÐÄ>[ˆ]4)õ×|rÔÄ<œÎÖüGÕ9bƒd:*dÓmß>Ô€:öçtVÄ—ˆ2i;½ñ= f1$¶œ Qã5¨§Ôº£zÛì9ʼ`l†åûÍ>ê§g€"Ó_º+ùðÑX»€£ :~{S`·ÖA¶ ÕÅ›,6pe.F"«iÚzz®aìWµL2“K’Ìȼ&Œòòh`§ËÅSštRäà©Ôså%mÞèoê¥{`aÜÈYÁëálÜe`iê SÙVz¤‚ûó`eóôŒ@ ·Ÿª41§ÛÔ|w‚AuÞ]ìM·5Ó>äÇÝ/î9 ÁZj†0Zʰ­ÄÐÎæ¸Vº›Üq¢ú9ÛfO—ÁoÆfX¾Óì…ĸ{¢ÖFØ|Å’¸=€Y0R3i­³¯o²¼üS/êÈVL˜‘y&«2󻈷æOB#·QÌ›z¦jMÁáúŸÖéÕM"§]Çk¿ƒ[u’š>x'O,f¹sh-áæ*Í—´ùúUŠ#Ý4`,Œdá&—Ô]ö|™j”#Q^œKkOpN™ꇚvš˜‡Óݠ駪sÄÎ÷b;{þãi›n[tIOåAnû£ø÷ÜÓj0ÄÐÒ7OÏìÞSšÃÚÔSl}lš½1ÙÆï4{­iÆ4Á´ˆ0HËÌhDƒy–Š'q3CÀæ}“åëˆiš4?i¬¶È0G¡Ìó-Ã˹Ô×´ÍÑ_úé§´0”%ßrÖ :ï.öpn{ËEïxnš²?|ÌÈD‘®õ3ÙŽ-Þ‹Uw“»WGæíÚf€¿è;à¿yëµ¼'&$ä¦ÔÙ"Š“Ì"†™=Ì;R+Ϲ!yñ\hÔôü×Xd˜Ž€!s:ÿh‚ÖøéÂ\ª"Ä|S'ÅÓ•ø)RÄ—´y£¿q¨—ìeëÁ`nQA¿rÌÍÛÃV-51§kUïíôƒê±7§’&å ç¶M½;”8Íï¬3ó‹Z}מX•äPæƒzŠ­“}fÏ›Æ<¥CM³‹-`Àf¯ß¥¾4sJ+}ÖX åR¯C œ§7nnyb²~[-7zN!=¾ùú%?Ä©5zI›¯u6ê;j`ßÀ²9ª@Ð4°8{¤—RIÚ}ʶrgg³‰Væát›*íN0¨Î»‹=–Ûv7ƒ[2¹0äVš‡Ô}oOn¬ï¹ÝM®¯xû¸9Íþ¸Ü`ÄÖî¤RNÂìÉB±GMÄ­Ì‹-Ö*›$!‚wî€l›ë3ÔZìU'ó‘HlæŽÆ`{I›7ú‡ziÀ9°xpï‚æ¬—R³µ3…ñ¦×_+ó,F/‚ϦÕ9b÷…[c¹íæe=™@a1^. prÓ§57¨§Øúqš½^ÂJHõÇm1CÍw³^ ã ƒÁýMÔ¢”MÄt¼•ëņYp(tkÚkåL§@žÀNmìÖ8«Þ/‰Íg}Tßy$’üî[Q~MÛœ{I›_ë$jºkÀ9°ÌG gÙ9°´öwè}­71§[hï„ÝAuÞ]ìáÜöÛp6ñÓ­[Ñ<'Ï$ënrÐG§Ù³ÞȾÀ«ó͘¹œ³UNóˆ.𒆨,*§‰­ôzÉéb°Õ b+ç”Nüá#ªãq ¿”‹Ì•~Iò)θ7¿ §çjÚ†á%mÞ©¨ »EÎÅ@j‡œË-Âß÷Üpºóõ?¨Î›³£ºÂm÷)œÀœÎÄJ¶$W„×ìøÒ¾Vá¬A=ÅVÇìYH…¶¸ÊÆbžë9Ë9[ O’'Ò+®öŠe1o¥× ÝD¤µòo´Õ™&Î0A _] k_¿$6ŸÅQ=ŸEÕkŹ𶡹¤Í/t»GhÀ3°¬ Os`9¢Sçð §;GÏóVÕyw±ÃmçVá//&VMñyžõóy|Êî&÷]vš=˜LjüVó¯1mùg+‚ÌZϘÍ⺀OC3MÄði¢¯ò`TЗBí9‰çç,lŸ¾=ýá2d#¨\ûm’¤Æd]/oÍɼÀ^[Yç^Òæ×j‰šîp,ÆR;äXºwê†átçèyÞÊ :GlÍVó¾ÜR·Ý§=®)¿h£ÌC#¾´¯•G8kPO±Uç1{ÐiÎ &Ô?ßjSõþÙ ´ ,äý8ÞÔ[æe~‹’7ᕾØh±ÒÏ9ùÈÓóÜ~úŽ]Eñj•\>VH©Xö­FIý\fƒ,…ð ,Æèaò, ‘FÙ §;ÿJ ªóîb‡Ûî¶=fUæV!a ™Ý¬ùÄî&÷uš=Ð4-ã0­äðý—ÿ—ÿTÛšf+`!¨’¿Œõ’ZQEkâ"Y®l¥Ï'Ú$g~ÙjYÄb¢ 346™Ýb—£Üq¹$ô+l/ióFãP/ 8–ÚèaÔ7 ,½ºs&Ÿpº3µ­¶Õ9b3õtTW¸íne2«êUp pÜû¶»%ìrâ žb÷ÝoöÄ„ÃlèA~›­j ´¬-œ‘$É,¿`wð¸£4ÖEaAà¯_(lHòí…¥'´X…Ü…>âM‹;YÍ›ž—‹§_Òæ‹=ʾð,ÆR<0¸ï• nh`Ю»Øá¶á¶º›œÝÜ9GýfOÖ®2x øS O˜IŠ) hwÄl¥,Öµxô’l¿½°Q`wM³¨-«Ô· üç»z‚6ì//‹ê"ŠÎ #v έ‡»ÖyKjÙúãzéK%“g\ÒæþÆ¡^ð,ÅÑè Ü러A:ÄŽhp¾ˆ÷-]6s±¥ÙÿõûRxf+âÀ Åý¸b0»T2%QàãëiëÆÙJ¯•­…×R· OÒ••±¼¦™×€ÞYª—TXR॰ùÑ[ʬiFþÙ<þàp[ㆃ ¥üg ìöäÊTÓÁyÀ|-R¢|zVjÏn$ß&¬‰/ióënFMw x–õ á©¹q`éÞÓî Ã麫t“á :ï.v¸í¦©Ô+±z€â]B)§øZ£ç×w7¹ó»°nÑiöà+ðk8hÑ`áObÂZ׋£@åÅvãlU‹ƒñ@qJµÕ²·ëNÍk$*«±!^ê‡]nŸ…lç+¢üúw;ow^V‚GvŠõ¹sâe® 8œE‰Õǹ` †¨Ž”`)AJ6rI›_($vЀs`Y žÝ–#:Û—g8]_}z¸ ªsÄ6FoOÇ4á¶ …8w5§ƒ‚˜%šñYØÉöÉõ[“N³¸ÊXW—›J¦-áÌõüuãlU„Á2¶VÕ²·võ™oNÑÂä$ØôΣP3©DSy½@í\F*t•—)£ÀnÏáK¹_¿üõ/>p®IB=_R ç7-Ogf•\Òæ åÄ¡^p,ëqc³æÆ¥WãNwœnkœÕyw±Ãmkb× ÷ê63»Ò5{Nñvó'ínr'Ê^mÊiö|â ¼DÀ_“”à˜§Ô³­'¯g«" –±e<‰™ÙOÝÎÖ˜ Uu´Hß}&ÁàÓgô {ãl}9˜šêô99ƒ¸õœd$~y³ù‚*­sù 'ED×Îhî’6oô7õÒ€s`Y›57,½:xŸpºãt[ã<¨Î»ï¤n[³»ž«ÀdJF"¿<°æA9É>eУƒzŠ­m§ÙëÛ¾\b®/ˆ‹2¿”)ד׳•n¬Âcl„ss4xqôü]äQ‚ÏA€ †P‚Ø¡a?³ksH[›&å(¼É–StzÖÒ.hÒû’6Ÿõ…ã4àXÖãÆfÍËq]îÅ9œ®—&ý|Õyw±Ãmý6³ ÔüNXLèhApÝî&÷jqš½¢ n­·§çâÊiûg«iù/áÆÓTDsk ¹‰EÉ…Ðk¡,šÝ—˜|û´òƒïX ÿNk®Ÿ¦´êLLÔ=—×…KÚüº›QÓ]Îeô® ö,Ý;y ÃpºcôjqT爽ž¶¬~n ·ÝÒuœk¡°“,`Ø"öØ žbëÛiöòË¿ÔËqëß"fòòÏV9[QI{¸Ëm‰¤ ÌuEâ1Xt^³(냉 ®rw°8ýbÎ…!zc{Àð<‚(º_Û}¼¤Í/.Mì¡çÀ²F¹›5þåˆ~À3œî%/šTçÝÅ·]†¡OÈ锾·'~1ަìnrG ìáG³'|ª*°§Ýúí¯Ø5$WVô D ´Aì?ÄÜÊj˜®ÄàwSÐ& eèîO©¿êN±Ñ&b8€®Ó ß>s"Gö"„s%ÔÄN²­¢Äʵ. ¬ÊKÚ¼Ñß8ÔKwXzuá^|ÂéÎ×ü :Gì¾p+ÜvíñŠú×/, ŠHK÷3Ï®¦Ú=lòœA=ÅÖåÍžîÀxʦÀl¬,e쨔lø´û>?в ›’Ð"ƒM1áI~ç§Y”…íód61r’$›I¶ô+Ëâꋦ»MÌu.‹¦tâ)©xf;‰/ió UÇîp,ÆR;äXŽè× <ÃéNPò¢‰AuÞ]ìpÛ…aìÛmˆ\íkà~gu7¹ûuå{Ë~³'¬P°¦§ÅîzÎòÏV 7¾Á!™´>X†—ßm/ѵüæ&i±l$ÓÚÈ–ÕØX/B9·‰øéyÝ,K×ÅÎa•–ËžBÇlÎ@¿J$.öqsBè¬)ðÜG¨$ý®%WMñ%m¾¦™¨ï¨ÿÀ²:ìÿÀÒ±;g² §;SÛjkP#vߨc¸ínÛÛ¹ÚÝÖOÔSlÝÙì¿~Á‹,,&9õõû ¿vòQ¦K^ÞÌ»jŸ–í@þúw;?]å?þ—ÿSø—ÕÉf1^³RYŽÐJŒâðµƒ¹Äæ®ÈMB“ŠÌ‘¿‰úKÚüúšFMw Üy`éÞŸ†Ó¨ìצÕyw±Ãm[mo_䪵•Ç¡ïnrÐ5§ÙCFî.K„±Z/±Þ˜Ó®·Ù Úüý¯þ#ñf„1ᬰ*·T¤^tÑ æMgßs!ÁX†ì ={ Õ‚'o-‰¢ÊàÌy€w3Î g0Ac˜³ÐY‘3mí`ž–†øðÕY‚½`鵨ªi"æ”KÚ|M9QßQÎÅ@j‡6–޽¸ «pºóÕ>¨Î»ñاÃpÛV½íˆ\µ6ñPôƒzŠ­C§Ù3õ€²Ø47qÖ|·8amÎV `ê;9¬šB¬fú®ÝùQÆ@/€“÷ïØØp¶—hHé Ó(€âl~I6sƒY—,eSOäTh飰ë¼kó²Ð;Å€'Õïœ`^n"æÄKÚü\!Q>HÎ¥8tØ•›ËA=:m8ÝiªÎ ªóîb‡Ûf“pvD®œœ“¬»É=B7fOd•¼S6ÍP‹Ýâ´µ9[-ª ®Ö2r÷éŠ$>-eã‹u0ßäƒTœ%Ùì5u笀—44¯Y”a˜9ÃßÏ>›ÌAïôNÑHÉÕæÂËMÄ—´ùÅ¥‰Ý#4àXŠC‡]¹9°Ñ3y†Ó©mµ5¨Î›i¥£ºÂm»(s3,Ö¥•»0ÔSl]= Ùƒ‹$Q(®KvA‘À¨„'ÙiM3s56^4Ko½½ýÙ#Ì<Ý—36¥Bhè…·¶ÿ{Ä^p±Ån"¾¤Í/4»Ghà–#ºyÏpº#´jóTçÝÅ·µíÄyt3råäó€dÝMîú8Ù#*I {Juäo‚OÀÏü’-À–Ö%.þ¿½µK¬IYÊó¤âââ&d®œ„"™*ØI%E|øÈ®AÌ!¯Ø6—ÒQp~†ú(°Dò½î’6ÿ½{Q:L ,‡é`'ãpºŠ»á´AuŽØMAM …ÛnªÈ f%<Ðé2>à¡A=ÅÖä#˜½Œ‡7.AeXQ "PF–;€’Í^ùA”€OR8Hš%ÀX5%R–ò•µÀ/§¤¯oÔÿkål$9°[£ET޲z›Ò-È^€9•6½G죞ž²„Q5ùÒ5m‹Ã%mÞPNê¥GXzõåd>át'+œæÕyw±ÃmwÛÞk¤kúžÓ½=±înåî'v7¹»÷ÁìY)³â’H­ôÈ ycbCz¹L¿Ü5«±)3|˜—N¯ËU$¡E!Ûœl sãå;½˜†5KU“< €-vC±Ç÷JiŠ«à>Âä’6_TNTöÕ–Ãz2ø,…¾¿ ð¼ö_ßkÜ65€9mÒ< bG4ø® .&V^žârP¦ÀnÆ<‚´·È0¨§Ø]Þ„ÁÿþßÿßÀ§Ú¯ñ2 ³•ÓCAž€1`çßüãïSÚÀ§ÏÅ•o[üŸ_þ[Þ„Tj1R‚áO´™Œ8ôÈL+"й Ø"½*1UL=Yû·‚«EJ*éäT ìÖ8Cß*v­ÝE½^rD½|P¶ûÈé—´ù…Zb74xçt ë.ö&0f|ûŒhŠŠteÜ¢ŸéuľØ2w79»¹sŽÚfO<‡›c3,ßoöØ ÁX™n¦Š0$Z›Rvß^sKX´ô¡·ùé)à9eäje¶´@qý@ža°>ñ\§ýD šUhQ6ˆ9 0çIl£Ýõ¡9ÔOÏqL^Òæ×:‰šÐ@hà=k`б3ôêrùl<`L÷›‡üx KGNf"œ¿¦X‘®“ÅëØÜ žbkÀ6{`0øíûöö†—€%¿†ñw7{b˜´š_Ølÿ c+ças56xá@b§s(a¨Ð4”6ž$dKø†úŠm¹>Ä­šT=KÓÍ5M®¹¤ÍçÞE!4  AºîbÛxÀ˜î7uÇf·„¶²ñè™BÓš¨ÖCžî&g´uÚ!Ûì9š10W–$^€1¯nåÍ0þîfŸ-øó5¡]M›ø–bNsM7ÚÎíò¾ž±ƒ9ÎH.ôºÅ9Ô‡­ÝÇKÚüZ'Q ¼g :Ð!¶=€·^SÓýæ¡îx µkGÓ3±¦7å§P!v/ù7¨§Ø×Â6{>ôF´0=ý×ЦÈ0ÏôÄäÙ’6`ÿAfFÕ¼íE!ååšég³•9œY* 16HiÃìPáàJøöB´Ò¡Éè.POU¢Ùû’6_ÓIÔ‡BïSƒtÝŶñ€1Ýo:<޹2 }‰,)¸ôäü8RÞ,Iw“»Y¢ œf&Ì£PqŽS0Œÿ ³ÿËÿýÓ €|BæìÖ´Æc#‚ÍjÀzTÁ¯mœ~æ4ªµˆÉpÐji¬NL庉bÔzT«çŽaØ´DJTž¾×\„Áhèžh¦›C4wI›¯©1êC¡÷©A:ÄŽhðƒX,“o +ñîÒ×/Ø}ÁúŠ1¨§ØJpÂ`žÄ3L\Üï0øÜÜ`:„£u2rIpã¥\ßúšf ± 0ó«Õüžž ãlbŽ04­\_"æ€mUrk0×y«óså×+òé3Èt™Œ_N$ƒºƒ¹ŽzpÃ}–Ú0^h½¤Í;µd¡ÐÀ;ÑÀ ]w±xÀÕ{ûd&¸Ä¦—ñÙ}Ù:JÒÝä:ʶ›•möà(p#)9x‚"ø ´#2 ŽªÙ<õG˜½–)£³ =a<ŒÊb÷A§lz#Lå´ [}¥²sN1˜Ó"Gó›¡tVà“Êøl£Ø»’ˆwNFÒ­ô@ÜuFñê«5rô’6o+3ކBïMƒtˆÍüÒñbÙxÀ˜î7:vüvVÌÂDØ™vÓ,l.Át{s÷â0¨§Øê²Íž£à:m\e %¯ÈNdã<¿†ñaö‚¦,;Fè´©eIìq™é…”`Ó·2÷r@’-O@¸M@3ÊÈl¯uîc}îf è§ŸÒüiQb•׿ ¨¯ï‰ ¼¤Í¯Õ5¡ÐÀ{ÖÀ ]w±m<`L÷›‡ŽÀc±Š/ÍJ¿þÝß>Žx%énreÛÍÊ6û9 Îxx^0Œÿ ³OK“MÈœteÀ0÷_vßµMJýýôÙ obž"äÓnÊ1Ð/i EþMb9•(„[ÀäHE¬¾F¬å IqÑ}×w¾úÜú¬KÚüº›Q ¼g :Ð!¶ÄØqAm<`L÷›‡Â;úxÄ)šX™…™‚I2Td숆îÎsPO±õf›=]~ͪUníê×0þƒÌ¯g©^^L#è ¶,†^ç]žÐŸ<æ,ˆÉ%p2G3z¢—Cõ;ozQÎ’d1¸…LIõ7ÿ¥ôã M'dþé³½T‹ˆ¡'œN™.í_ÒæþÆ¡Ð@hàj`Ю»Ø60¦ûÍCáDZÕô¢Íô‘%6fØÇ‘­£$ÝM®£l»Ymš=AWmØ9À)oTÚ–ï1{a‡¹ €Ã\^HÆÈŒOçs¹AÓB .ÌùíEËJäoÕe>‹ÂZòÁæ®n!‹d©_ß^èà|£9y~’pk@ FþùÑEb /ôiUá·ïñ-hòî%m>÷. ¡Ð@h :Ð!öíÓÐÜ6ñ€=éG=x`.Ɉe®ó/¿Dçx[gÄ.lÊ<¨§Øýò›=óFìH|£Ùkй„¿ùÍ?Ày^³(s“T%Ï 6ï¹hš´M«F°»`8ßÕ³ ¥7ð•äW”¸…çšÊ F`ÞLÔ—šQe*Ñy¡ iyû㉠do{åÿàd¸éRfeI/ióÅžFeh 4ðn50è@×]l?0¦þâ¡÷ƒçîÓ÷ödÎù¾åî&wßî¨u§Ù p&09ÛÈ/-¼*=f ïŽa09æ©—¶ ÍHEwU6ˆ9Ø# ×Ùòú`´U;äI²±Á ƒüÅAk/Î3³%Ô C v vq®v œžª<=ëKÍ –úL™BÓG„aC-|Öµ›S¥Þ¡#t C: ±¡KÚ¼¡œ8 ¼C :Ð!v_¸åÄƼ_;äÁïÐð†ëò žbëÙiö-ð’ÒJAhKAP# ì1{Z‡³bžúUC†Ì<÷O€pBã<Ü·S[e Ðk8qþ:ç¢!Žæ%þä—ÉâPYŸåÝÄñ)°xrÑvÈ-z·‹Œ.³KÍ%¡é¤·i-;}ˣ؜*ú\>•y›•s …\Òæ åÄ¡Ð@hàj`Ю»ØN}n½17¢Á4JØV‹f—2˜¿øúrB ;‚¼$ÚZ`jè”y-—”1òôl ÷KÚ|Q3Q ¼[ :Ð!vë¤c_b'0àníPÀ`[ó£ÔSlõzÌ>¥éã¼$ K+æ„Ûa0®Á6—ÐFqP‚â@n,Ë@Œ4•ÍèÙÈL°(›R;ÌÓRLõÃGåÍR`7ÕTð­r'4 !O Î»¶.0Xb“ç@ÒòÛùö€ÆÅŸäê"1XnÐДì1½ ¨þ®Ë5—´ùÜ»(„B¡40è@×]l¨]»>`ð5­»É=‚ZœfŸWÙƤ)€‡õ"q. QÏ+7Ë—´ùÍ^Ah 4ð®40è@‡ØµdßåkÂy®÷üx`ŸäqÖ9ÔSlå8Í$ÉÃôõfØ¿ßìyF¯Çôø”"–•t_²dÁx¤d Œð¿Æ è±V¢9‚ÄcO’4+” Ó fÀI!—‹ô´›_Í#}·ÖÇ|.)ÏáÛ gåWäòÑeáÛKzmpÊšæ—;…%ÁÞýKÚü^eÄy¡ÐÀ550è@×]l'0æýÚ!?¸¦…]¥WÝMîã4{!ÊZm5›§Þoö [Þã1=áJ~¢EÍ䬌„TõYç§gÖÒ5ˆH8yŠ3ó«%s‹Ì©ÇÒAвIs 4¶¼bO*agì]sº¤ç´òÃ$‰-†˜àaÎ}š—ò×ÌsMzõïé´O’CÊ—6ÅÎgy —´yOǃ&4x?t C숿+}„žê)¶êœ02a`–œo·Ã`AÄùÒµy9¯…äø;•õÁTæ]9¤"`A©]É ’œoЉ©Ì‹Th‘aj ž ¶8QèyÈ‘rÖ’ÀŠC)ü;ååfI 1Ô ›S¸/ §êæ¢õ¼›Ñ»j ·ÅÎ'z —´yOǃ&4x?t ë.¶ó~í?,ö~¬nÄžv7¹GP‚Óìµ²ø³fäëz§Ù§7¼¦çøDM «þîïþ3mšv"³ æ\€_‘2P(H2oZc­HL¥$¡Z’.XÎf=qWb¶ð¤ ø.Ûš³âÛpÎ2¨Æ†ÁB¶9—ÃN¢Èè]­oнÒ¨¹¤ÍýC¡ÐÀ;ÔÀ bצ¿}щÖÓýfì;Î:MƒzŠ­§Ù“´À†°v^=Û´yüf¯uÏÈsð$‹ÎŸWK©¼õu€€dfeK Æ"XÍ*‚9• ùÃt9*ò” E²E¥ k!²ÍoÂÜXÏ£¬W긕à&‚ì‘E‹»w/ió»µ'†B—ÔÀ ]w±xÀ4~¡ÐÀ540è@‡Øó ñökC Áxæñw ÜnfÆÁ ƒÿüÏ^sƒ,Õ/ñÒôïúa0aÏôúØÛ__¿~ãúöŸ—ÔŒ¿ÙQ2ÒJ¥?¤ ,û³ÎÐðº4l5¶‹¦튞ÂâÐbWèŸâä¼hþÑ/óøñÙöc'4 \NƒtƒŠ}9ó‰­' &ü› ï§ÏÄHÙȾñcÊ ³}{Nƒ™óz} æÌÀd;ñ˜£ÿôÛ_AÏF>…Ai¬ngáa ù±8@™46½(GasM31|]ãÃÇœ Q”‡JdÐ7 é 52ê éȳN JÅ0kë'ކBÐÀ b5ºÀ¥.„¶4à„Á Â}ø8ÿt²ÖI¸ó¾‹'ˆ¹VQÅmBÄ­>½Òƒ¹PŸluÄ D}tƒ_v‹m¡´W@;­rœÁm‘X•ô‹Ô†,6î#àlQ² ‡ bÊصè'ŒMaÝ¡Ä4 7îk^ãÒ³øZþAg‡uG¢&4 Ô40è@7¨Øµ«õ¡»hÀ ƒ'¼Ç+iJ{©¥T7Â`­Ÿ"ªÜkÙ[DEB–žê²•̆žY z`'xR`µHLìZ)`TèI!6Ö4‘‚f!Ë Áé ¸õêj¹-­žÁYªÑéô%, `Z¨ÊMæê ˜™hÊè„Wö ón ³YQ „®ªA:ÄÎÓÄU/Mô+4p´œ0Xëß‚šð;0…´}úð®¸wP–g×`0çª ­VÓ <á–¡¬®ŽÁ<¿yG'¢%ƒxÐÙ¡xÕ¢24 50è@7¨ØÅK•¡{iÀ ƒI‡` žþþM_‹xzfׯü0˜Hf†Á "V¾"w»Š d s!ðô ãéØY[³BßË —¼Ð ”B¦äj¥9…Íî¹ œRe£P”¬·çÀØD¿É²†x3âÍœ•>¾üôÌÝ‘0†Yû2ÅÑÐ@hàt Cl{¿À¥‰.„ŽÖ€+%˜_"P™Â"ö»ÞõÃ`b¶lúçxl÷¾§(k}aÆá|<4Fb¿ Ir¼s,:ì ´¸¡ÝwÐ…¬Ÿy[ và´p2…€‰ÏL››£lð$ eóo®Mâ  „B#j`ÐnP±G´ù°a0ñ^‚‡‹À£jrªðSã‡ÁR/±M6§>w\Pb> þóÝ\Ÿ Cë@Ó91Ì3ŸÍBÎL0(aŽ4¸Œè»6;\æÒDGBGkÀ†ÁÅå‚NÔo«Òƒ¤ CËF¡ Onê9I€-èÝÍSœúp›ˆI¨P°óÜM2”@~I°EìâjÀ ^áö¹Ææåb+€äùÈiß :;;•¡Ð@h ¨AºAÅ.^‚¨ ÜK6 &ù$U¶×Wäžžµ›»À`¢Ê)ª9½sG!¥¹öûƒ- ÄKIcHiý`0¬R"ñ´ô±òZ¥²ÖP«^£Cæ´`ÅÓ³±9 &g£rð;%Ùü¬s ³NMYh 40®è{ÓWÿ!yhàŽ°apF¹|-ÔçI Χ8£Áà@8ó@ŸeÓÈÁ Àn ¶*JK.¼‚UÈýú….´2©ÑªIÇe¹c–PÖ*ÊEJÖ” Þeûö¢~¥ß¯_ÃYQWñ,i[‡È `×X0M©ÈlðÛÄ€äÌÊøDˆštv(ª4*C¡Ð@QƒtƒŠ]¼Q¸—œ0dÅÆ·Ì„r–îÖ NœWo à æÞ–†¨ì¢qXÍŸçèÂ&D_ÁŸJK0Ö †@QhÄP†¿z Ôº¸—B–ØyÁ4ÕÚ‚iHB9QYÈÁvÞe]G@8¹àpvçGåf ‰ÝÐ@hàzt Clc¨¿ÞeŠ…ŽÐ€+Ú xcñ®ùVÃÀÔ;a° ß&j Ö¹½\T Ìã0€"¡fi5Šs~ÓÍÎ. ;P¾fe|úLÀ–TäYÿq’‰~GÏúÀ4°ý¯‰s ˜6áçŸæWÙùP.p Ÿ´(Ü´À¿J œU…Ð@hà}j`\ü>¯Wô:4ÐQNÌ:fà¥õv; ¦/)ýubÎjl€a{uP¡QžEmàí.ÌÄ[Š@×^Ÿ!“¶5þÐ'5}`z«Š`§²³å(è—lEw‹Äé ~úŒBä pÝhäP|M¤Ag‡Zw¢>4 ¬50è@Ñàõ¥ŒšÐ@«œ08Ewÿ꼺غÀ` Ì N’Z~[{aÐÃ΀@~ØEˆhœ^;„ÌDws4¸FÖZ¯Å»Î¿)_Úü¤ˆ03D ñbÄ;gMÌEÓ‹yÍ¢<èì°èEì†B¡CƒtƒŠm\ˆ88_~L*,!Gp/+9(Ç•X¨¾¦QÃΤˆ×.O_@#`[\ìFµüßþ-¤À{jž˜­¿9ƒeKoÀuýƒ§^©#Ïm#†Ì2¿ÔtÁð¡ uðO~ù— mÊb~c¯ÓÂÓËeðì»Ûb]2Æ"2ðdkGæYj‹ßp/ÉÏ:ª¥ÛŒç“ëÈ/”ÜP08:;Ôõ¡Ð@h`­AºAÅ^ë?jBwÔ€ï,±i1. %©¶@/Vñª…‚©÷Gƒág 9 aò"h°}£f YwWßæ*îVÅ9/í«uÞ <ÙÚºœÒIZ øÓçI’q¯$©Å¥¥-sÔn/ÅÃlMíQ \FƒtˆÑàËatä^ðÀ`àèëǔ߲‚…ŠAÂ]`0|`VßTøô¹‹ƒ“nA†ô"Áø:žl½â¬uØh¢¸>C+ÏLB9ëþùТ œóÉõ5­pÂÕ>’¬V 练Îó.D94 Øt TlûZÄÑÐÀÉðÀ`°nzŒþöÙ…TO¿]`0¬´ÒWΆ¥P‹g¶ê>Tš`Ù1À|Ç%Î2g˜ÓD/ÕA¤ÍÚH)Íæ‚ié®aŠÕs®zjh)åusí¦¯x°Z'Ä1ÌʉC¡ÐÀ540è@‡Ø]‚E׸ˆÑ‹ÐÀ> 8a0p—h*À <œ7^°êƒõ=5%ìëÅæY`T†‹î# ž$ÛîœÕ£¬©¨ÖM@8€ä Á&—Ì"£lß :;Ôõ¡Ð@h`­AºAÅ^ë?jBwÔ€³zOÛó¦]~»À`ÀËäòŒž»wTˆ¿i‰­ÍÒ²cVx“™9ü=:9Hi1Ìúí!(C¡A50è@‡ØE`½Ž!vh`‡œ0˜tÓ”±ÚºÀ`àtzLsa{[0mG_N>%e?=†MËŽ==ƒá; °ZZ-mK'Hòº°ð·¾Ò:;t¼Á*4¸¼èûòæKN¬åÑÀcd–ηÛa0˜ Ì}-æ@ÝŽ@î¸Ëœä‡ˆ?©ìvYƒ†ÒÉ‚yU'_¿ ºôà/Ò›€l)4Ýé/†ÙNŠ 6¡ÐÀãj`б#ü¸V’ ¢' ÖW3fî]r.˜¦µ Ðﯱ´/ðd^þëAµøõ ƒræÓX“Ý^ ¦-Ö@Ë«œ­µAÐøÿàhZéÁ$oë!½$tvX+*jB¡Ð@MƒtƒŠ]» Q¸‹œ0 ÅÆ“wa]¾ï¶½ë]' žÔwÍ€ÁZöö‘ïpé;Wb³Ü¤u̾~a##ÂX¦¬õÊJ'¤:(ŒÚQ~Q'Ïy{Ž;®£Z /·Š} ³;”§„Bci`б{ õc]¯64ÐQœ`ØôédàÀo¾­Ño®qÂ`ú¢Oú‚ô8÷ÿø_ÿ’.:v°;+VÌ@Ôƒ%Ÿùé™ÂkÙ\y¬U š`ûó?Kà–BþH\‘ªÎ0¸ïÚȃÎE-Eeh 4(j`ÐnP±‹— *C÷Ò€ƒNaf‹ß z×? æ~–§ù¼ ÆÊc@b~ï¥ O»HKv±¥”fsi_ç9 ÉÆèDª `ßòóÝ=r!t:W*—ç ÷•c˜Ý§·8+4HƒtˆmO ]‚54p/ 8a0(€ºÞÖè7×øapÊ+xû# €ï²½í=ôÖ1c‘4Ö¶¹`oSgˆÀçÃmž8 çeãÄ”nñõKú5ÿÌ>ÅÁÐ@h 4ðƒèûÕÇNhàÞðÃ`ð-áå™»6vIåÍ w]ðÃ`r-Èh˜Tjë½µâjŸÀ8’³®¯–î†ÞYøÛ É!(P‰ÇóÛ—d[DˆJ2³„·Áv ³[ºŒã¡ÐÀðt ClgÜcø+ ¦? 8Í3"ÈÎoÌ­105~ [˜ OòËîaÝíÉXy d,°¥ï5wZçÕÏ´ø •x ^åÖ£—èà^8“‚‚üˆ s#&<èìÐKWÁ'4xt Tì÷`QÑÇ4à„Á'ÁNiã½°éu9Ê ¨"V¥“Ú 72.À“üRB|¨"Ë©%κܘ+ë0L–¯ÊZÍ«Y O«–_¸#÷mËÓÅ0ëQiЄBCk`б»L:C_»>4p£œ0°Ä|bé`–J0£ÆÈ‹pÂ`¼˜À2èWÉËߨ¯N§û¤p¤†¾½.ÂÝAÇFÑ—F µnp—¥€+ÿæ7ÿ@/ ΃Î/D° „.¯AºAž¼9EÇÒ€O;žƒ^}^y^³ˆ ;a°0‹ñJo@2žÔ¡C$‘U²8ø¥ ˜ï(6w§5Ð:-C!L6‹Då{sörÇ1Ìv¼¦Á*4xL :Ð!vDƒÓ¢Bª4`Ã`¿@&°.hé‘A™>^Æ.j}ç»NŒƒñæ£Ð(~Í"iJ æ7…Ç;!UëNä¥'€Ä9Z~»i ´ #3½0xί‹A‡B¡ÐÀ¸t Tìqí$$¿¤lÌç@MÚÀ½ló2»sÜ»(;a0Z%vè»¶-p¡^¬v©A9õ£Ï. ýf¤]“3†Ùšf¢>4¸Œè»ãÔp™« 4iÀ†ÁxYB¿SFDú]m è;ßõÃà&ˆøÛ Kk ½^a;¡ƒŒœÿýïþ‚eÓØl±NÐa4 \FƒtƒŠ}³‰Ž\C6 &)‚wâŒmŽ{åËÃà”òôÌÊclØÂ$À½ý‹ií»éîÆ^Š-†Ù!®i Ü¢A:ÄŽhð-×=Î  óÚ^Æ[T %È(hïåÂúÎw¯ ƒ“ÄɹA±/»vdõAìM/Ü!0›K± :;<ˆªCŒÐ@h` :Ð *ö&B¾ 0L ”Ýý‹ñ?³¥ìˆÕ6ǽ‹òµa°–\`™ ™ŠvxÄ¢„ÞókqD¸¦†Ø1Ì>È… 1B¡ã40è@‡Ø >Î*‚ó;Ñ€ ƒù°Åÿô¿ý_ü²X…õ¶€¾óÝkÃ`H"«Š§uƒ?||üIËÓe9÷wéc%ywmóƒÎëŽDMh 4¨i`ÐnP±kW!êCwÑ€ ƒç°¶µ|mÌÅRlœÅ4TÙq–ð}Ýào/¼%ǺÜàÍÅ0k('…B×ÐÀ bAŒk\šèEhàh øa0ùÀ|!â—ÿ<}@™ ÊÄB l|yLz-+ó=b®…Q†#p/Ì/‹§Ùk¦ :;í5Á?4¸’èûJ–}¹€œ0XßËPüóßþ›¥Ta0KIÔðåa0_ øÇ¿ù‹ál¸®¯~!LF‡ýZ_ ³Ã]ß84hÕÀ b~i½"A8MÌ’Àä+ˆ÷¥5„?|¼ýcʧõ´gCß^~þú…oêõžÊìöä4/¤ýúåŸ~û+=&¼O¦D­ÁAg‡Zw¢>4 ¬50è@7¨ØkýGMhàŽðÀ`â½ ^> 覆˜ð»…Áôýÿठlä"rÇëèoš·ùô)@®fZñøÃGãÜf åÄ¡Ð@hàt Cìˆ_ãwÔ€ƒ—H S ª¼ˆ÷ æëÏl€aòlUÞ\€÷Ž—xÞ´LK«úÌË} ¡\ͼìÛœRåAg‡uG¢&4 Ô40è@7¨Øµ«õ¡»hÀ ƒõ^X°SÚžžk‰ÁÔ_>7¬e"¸p‹…Èîr)=JN(¹‹áÒSìÑ[ЄBÖÀ x±#|a³Œ®£' æU8ƒÿø_þk þô™è{†Áܰ¢².Sú4›™]pÎÕÜlEáßßÿ?‚ð¼áÈê\Gã¬Ag£Gq(4 ,40è@7¨Ø åÇnhà¾pÂ`Á]À©DAt_>ÌRcyµ1Påë+)‚0>Ÿæ—ÓòåŠvÃlQ-Q \IƒtˆÑà+Ùaôå.ðÃ`‚‡)ü¶é¥°÷ ž_/Æ¢Q†#VK#Ÿ™?0ü¦ÌƒÎóKåÐ@h 4`k`ÐnP±íkGC'kÀ ƒy'.àT˜VK{çëŸ|¥:6g¯,Ɔ +Rp„r09l'bc£å6ñìlyÙ?]õ+=Ë fºg¦ÞzªôöKÑÓ=ÙTÔõ”Áë›Ðâן}xp÷úWŸÞÜzó¸ÃC‡ñ{B€oMt¡aëpx5”ÁkjÐVÕ½¼¹S?ˆ«³ êߺŸr#*i¶cs„&º Ûjð3P/: (ƒ;âwiºêÞ*€oüåwž\yï㪄 ¡G‡.°%@ T 4Ñ…†:I„=«€2xÖ‘ÝÚ¯ªx«îýÝþGËÞåu'å­o®Òìq2¶ 0@h¢«°­O3 u¤—€2¸—|—v«â­2¸® ±´þðÞÍzÙH¤¡G‡.¶%@ T 4Ñ…†:I„=«€2xÖ‘ÝÚ¯ªx«î½µ{ùÉãõ†}gçÕ½[[ß¹l”f8v 0‡@h¢«°‹s ^¸heðE öýUףƽûÞîí‡ÿnzthuÉ>|S 4Ñ…†ýM{¯tPw€u›¯¥ƒ·n?ªK?­~?hü8n‰Kš]w|´F€@ÐDWa[ î0]49—€2x®ñîÒÕëïÜZ’ç—>ÿÿë§ýxèÑáhGl!@€Àq¡‰.4ìãFÁv]”Á]Ø;6zmo·}  Ö‡ÝE®ãXhšBëÉ ÛjðóG ÑÊàèá;Eðubp­/¼üú•W÷þû|ëW…¶öÅFlMt¡ao ôP÷’ïÕnÝF¹Ë߃ÿ=ß4»•ÅFfMt¶Õà™æ¡¾tPwa¡ÑÊŸu3僻×Á„=²‹¡‰.4ì |/ ôP÷õ_¿õº.ÄßþðÓzüý¯-W¨;,†4{œŒíL#šè*l«ÁÓLBé%  î%߫ݽ×^ªŸÅÕ"ö÷߯›)דF$¡G‡Fì"@€À†@h¢ {ßK}”Á}ýWn½–‚«î­sƒ_xñåßþæéõ"\0må!У „Ö“¶ÕàÑæ’xâ”ÁqCv–€?ÿâŸU÷V1\׈¨+§=yü@|OŸ%@`Ü2x|] ÐW@Ü×åÖké êÞ[»—«®'unðóo¼Ûˆ!ôèÐè‘]ØMtVƒ7ÆÑK§PŸ-÷#OËàKWŸ{åF]*­î¤\7•;¼xÚÖN…¶öÅFlMt¡ao ôP÷’ïÕnÕ½'?Lší5LÚ%@`5ÐDWaŸ<™¯†©!YÊà¬ñZ9ÚУÃÊJš#@ Z 4Ñ…†=U?Ÿ€2x¾1=ÇI³çˆé«S 4ÑUØVƒÇœQ¢ P Öú¡†Ö‡Ò"¹¡‰.4ìÜy"ò)”ÁSëyuJš=/IßC€À°¡‰®Â¶<ì¤XŠ€28e¤ºÄztèb¥QBB]hØ¡“Dس (ƒgÙsé—4{.Œ¾„‘B]…m5xäy%¶epÄ0õ 2ôèЋK»$ „&ºÐ°gˆ˜'PO<¸gïš4{vCß@€Àࡉ®Â¶<øÔÞøÊàñǨc„¡G‡Žbš&@ N 4Ñ…†7=<·€2xîñ=cï¤Ù3ú8ã „&º Ûjðø³K„ƒ (ƒ ¾á…ú¢i,ÐDvÖÜíôÊà釸,”fÏ¢ç³D„&º ÛjpÄäÈÊà‘G§{l¡G‡în @ H 4Ñ…†41„ú,Ôÿ£ª„/âqmo·¾Ü_ºÀ³ð¿@ x–*K'v¿Â¶œ8pb&@€ "[(  @€DÜ2Øjpâ|3 @`Ü2x@a @€$ ä–ÁVƒ盘  @€ƒä–Áƒ ƒ @ Q · ¶œ8ßÄL€D · P @€‰¹e°ÕàÄù&f @€À ¹eð €Â @€HÈ-ƒ­'Î71 @€È-ƒ @€@¢@nl58q¾‰™ 0ˆ@n< 0 @€rË`«Á‰óMÌ @€ArËàA…A€ ([[ Nœob&@€ "[(  @€DÜ2Øjpâ|3 @`Ü2x@a @€$ ä–ÁVƒ盘  @€ƒä–Áƒ ƒ @ Q · ¶œ8ßÄL€D · P @€‰¹e°ÕàÄù&f @€À ¹eð €Â @€HÈ-ƒ­'Î71 @€È-ƒ+r @€N-0HA.  @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€æø”Ë0 endstream endobj 100 0 obj 176618 endobj 102 0 obj << /Length 103 0 R /Filter /FlateDecode >> stream x=޽ Â0…wŸâu0Í !IWÿöBÀÍ ÄR£ïoRÁ{†Ãùƒ»2±¢œühxÍÜy2œ«êijê-e|¿ð3ÿÊ·¡6»T8EÄÚ-îl‚(qÇÑ©`‰…áf•Fˆ™ýe¡.e¦¾?9ˆ®±}3}P!s endstream endobj 103 0 obj 121 endobj 101 0 obj << /Type /Page /Parent 85 0 R /Resources 104 0 R /Contents 102 0 R /MediaBox [0 0 612 792] >> endobj 104 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 106 0 obj << /Length 107 0 R /Filter /FlateDecode >> stream x­”mO‚P†¿ó+îmêÈ _¦mšmn¥i§Z›#<*-P¸òßwÑQ®yøÀÎÛs?×órÖa U|WDƒQ×À^à¡Üõ l?ÚRáÛá)…¨Ñˆ“™!.ªšd»èP-4¦F™T5hÔEù¶¢ˆUЊ=ËßbÎEQJ ïèÑØ‹Äbd"™ýn_&¤v(ÐûZ-y€§±yÓé›÷ÃAŸÇæãè®Eš%éTMä0Éz5Õ”b¨Ïð̼©ãÍ›§Så*Të5é lîsæ1îØf°]1´~îÒ ˜P±,·Eh'EÏrʼn6 ëÅÆ “Ò9ȽhïÈ3ðÖô#^ˆá³fè´7 &}}è)vÌÅÀt¦—ÙÝûò )®§8th72¹–½p<&|9=yí"ÕHkk×0¡èŒ/Ý i¢%ýÕš¹EFHý@K*†ZŸ &ž c\N¸NTÿý ÈDß«¡=Â%Ÿ2Ž·mVø ÛgÈ,©è)uRÃÀÊâ–ë7ÒŽIYGß©/Ç endstream endobj 107 0 obj 404 endobj 105 0 obj << /Type /Page /Parent 85 0 R /Resources 108 0 R /Contents 106 0 R /MediaBox [0 0 612 792] >> endobj 108 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 110 0 obj << /Length 111 0 R /Filter /FlateDecode >> stream x­”_oÚ0Äßó)îaR‹i`ª¶ÒÛ[K'oS¥J(3¦¤›mjU|û9NÈŸ´NŠóÙNr—ß]òŒ;<#rÇGc4Ž¡9~Bâlj˜ñ[ Ë® IäG¾XÎFîÆ(˜À' g‹ü¹G†1âATàìË t« +œ~åF nש|„]'©\sZ†a' O˜ÑÜX)âŸZÎrI‘ìr^j¹¦ØáSêwÊv·á¸Ä.—N2™kµÜ2Û»zäöáT&Âí^ádê¯>yè\tàUéá¿õ‡•# £7 ÿÙÅüÛíçïS2eì‚qiM·\‰Ü®f·šënáälŽÅÑŒêN|™“bìlr&‹tÙE Ç•hÈ^ѧ·7tvCô~>s ¤u|J^¾±›¥SQþUÊ£ Õâ[ø< S I‘0×Sî¼´@ÿ¼_(¾†Ls¥•Ø'ނʸѶ2ã'•Êl(¹W «ÄqÙBâ$ªõìmäèC¼_ „ÔêW£þâþ9¼$Ì¿~\·A _ëX€ÒK®ñkw€|ï:8ܰM¢a&åo­” îþdŸ endstream endobj 111 0 obj 456 endobj 109 0 obj << /Type /Page /Parent 85 0 R /Resources 112 0 R /Contents 110 0 R /MediaBox [0 0 612 792] >> endobj 112 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 114 0 obj << /Length 115 0 R /Filter /FlateDecode >> stream x­W]OÛ0}ϯ¸“© ÓRÊŒ•nãò±ŒiR—†-II\¡þûÙNìÜÐtŒÖá!­ÓöŸ{îñánà:üïÀ!ÐÈ(ü„ö‡¹A.u ħl§#¯bQ¿ëó/vˆÄðÙ‡ˆëÈ{Ûé ]¼ö¿tm¾ Þv¿RyS6‹’G`3ŸA”Ìh±Ü¶í=Ë{‚‘WÓEä¯êwEIXS²í8‡º¦UÔŒ—ð!HÓß°åœÂ1ÜÑ$äõŽ®³4\¬}òHÙýnâÇüé ì å§wî÷>îDdIª4†w!r{"p,Áð+§hÀàúöêüÇгƒ4g“€&,o鵨OS?`‹Œf­Ép³®ín#‘ýHÊK!™œL¢°ÒTEˆ¨Šü>¼{£±7ñ~]8 ã È.Éòµ§¢; Ö[¢\«>RˆT¥%ûQ‚ª•Œý€ë”r,Ø?t+"Êi5§Y«Ž¨2¨©M÷ø)’Ù&ªª]uŽ tÜé ­¶¼$º‰ïW0@‡ã ù!Ö_¸çÐ!v1ýpj‚i 1f!ÍàaÙÀ¼‰½v›6÷3?δ­éR›Ï3@,‡è, ×Y{KìùÅ_B:Bî|>£!páÍiÆ–Âú £öfýû´Y;Ø„ Ý• ¸÷*¼?ǾI³`æóC §ozÿ6ŽKz¯yæ_Zÿåèvøíl|~ñ}dGIΉ˜„œ–àq…TX¢q’$$Îr qñÊ*ãÝ8 ´I¿Ù›êN`+Óý“>‹ÒŸv¾xgø¤8 ¶ äif•Œò4Ð'“8ÕšÂiäp ƒfŸ*¹Qpj9¡Z­'…íiqbT EܪÂê•f5ÁæÅ¶K4‘[¿bH¯niD÷GjÃ̘¸8·­*Cmœh½ÑÛ˜ÑdF@ƒ‰¨ÞÔ,]œËJ³À1A$TÓ¾ßM.鋪P§µ™¯Ä2á”OLu»LP}ˆB#ÙÅÅ)mµëhŠ—ª¼ƒBêÎÆƒÐãpwdbÖ”AI#¤tßè=¬Ì¥fº®ˆsŠ€Â¸ROEAk»(»YN'9> endobj 116 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 118 0 obj << /Length 119 0 R /Filter /FlateDecode >> stream x­U]oÚ0}ϯ8/E‚4Nù(Ltëë*m JÑöR) Á·‰M³ MûïËq2HÖ¬Šy@±!÷øœsÏ}ÆžaDŸ11™(~€ã|¸ard tã_éÄHVº©ž†Ñ Ss}|²@Hü2#ùî’¾ ³oÀòqþ¹§G»°Ö8»¡/Lnàp!74Àã+ˆ5Ä–Ž®ëmÍzÄÌJá©RÉ»ÕSZ…»—ya--ìïñ‘Iꇘà;å+ÆÆã©à’rÙ½z òþÌ®#™à6w|ŠÉ~µQI]2Ê¡8@­•iy쉶: saäpÌÔBƒŸŽ·KHi¹‡¿ká7îÛïi‰“”8ÿ%UßÌh±I€zÔ•˜.æÖlné¾ãn§¶Rˆ­:ê0Ü-å~KÕ:Å´›¶Ò;ƒ‹" ÄµŠ¢ ›Ç.âÈ@²U: {9ˆc²BŠ ¢»"”¶Y9ì ä˜…ör·w–mÂC—…¶®Ís–Ô+…÷Y®˜öZÌT*6äd%=¦E:¬6N¶" ¾ycØu‰1<‚;ùuø…ÓÈÖð³>©ÃJž‰92´£@ãøv=ýr;ŸÙ_Ókëv1/q!ϰ”54& ´ é½)ßNAÊ6¯ÍÈŒAÕ;/Ñ ¤%ŒéÉ Š³êËW=ŠEí!ÙD°ŠÆðr¯´É³­‰  iª.¼uÇÇHgI~µ»?¼ ( endstream endobj 119 0 obj 564 endobj 117 0 obj << /Type /Page /Parent 85 0 R /Resources 120 0 R /Contents 118 0 R /MediaBox [0 0 612 792] >> endobj 120 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 123 0 obj << /Length 124 0 R /Filter /FlateDecode >> stream x­V]o›0}çWœ—‰¦J(¦ùì”n]”m•ºD•ÐöR)rÀihÁN,Цý÷™o–•U˜„ Üãsν×/¸Ç tyõ‰ÁÈ€Ïð“€À â%½¥=Édþ4ê†byød‚ègz|§Ãôpñ¹«ÉY˜+œ}a!vN¸å"\3Ï·!VæÓPøš¦µó S3—‡Šÿ?%q"pgXV’ÀÞyÆøÎ¸íðÇ««‰à!ãaçú‘…g®°hè¾àÔc_ãW u‘œ¢ CF””Ô!Õuž™Ún€˜K½€“2S ~Rw“¢ZkÊß©ø‡Öû” %vR.ÎIÕ3 D Jd `.³BLæ3s:35Zk‡³E®c·óÅ`» ÷«Ö.)¦üÛ´'½Ó¿,Š]›S”²ã,Ê82ŽÝ€NƒnâP§,PÎÑ,„ KZ9h£bÙ Ëíž.]Ö„‡†¥´®Í¥KæVŠò¬PLy­ÌœTlÔ/ÈŠsL‘JGGSRÅ’oÞXì:DÀˆœü:ŒFüBÈqÉV¾ð²<©¿Ã“<c¤+xÇ·›É×ÛÙtq7ŸÜ˜·óY… !x†¥*¡1n mH÷MõíȤ aøz²Gf æ¹³“U0¦Å(ªýøP_¾Ó­¨ÿW©M+›ðmÙ†—û\›¢¶5±áA©šæÞPŸzÁ’^Rk™3å>⃆¼Ç ÙeûÃCgžŸãNP;…/vHHVËÝB•gµtp¸šb‘ íþ_D. endstream endobj 124 0 obj 611 endobj 121 0 obj << /Type /Page /Parent 122 0 R /Resources 125 0 R /Contents 123 0 R /MediaBox [0 0 612 792] >> endobj 125 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 127 0 obj << /Length 128 0 R /Filter /FlateDecode >> stream xÍXÛNÛ@|÷Wœ—ÊP%Æ—\© ¥m#Ñ T«}AŠgCœ]°7 ¨ê¿w}Y{ã 8`hËvÈΞ3gfÌ\ÀèüÓ3LèMðü£À7ˆé¸á·4CŽøfzÕç¨›Š»‚Ï6Føcztn]Ì®ö ¾t4~ìì}E 0[‚C(["n0™]½E¾Ã¨¯iÚ¾b_éÃK—Š~;½Š†Š…Ûƒla%^xµO˜¡UGð‘9&W‡‡#J"¬}|…ØåžG]‡aJ¦ÄY!8:†ßûPIU ÚÆ0ƒ’Ôj¢ !ÕÃ7Hm5PKÏà$•©…îoEu—y§Â¸ÜÿH‰˜”6g§VuÍ JH€yÈe0:ŸØ§[[9î4M;„ç­ôa°ž±Í-ŠºÖ’:¦õ;ˆ|ŸÄBi5 Í¥›ºœÊA Jã`:[oœ™‡šàÐ@ëØ¢ ÁÏQÐà.Ûä™ùþ=œQg€>}€¸Èªì*Ϫ0Q,;Z‹iꃴlj£ý`ŽÏB$áÎ=yp·Fþå¡‹ùÝ#9•ð¢³kî€A–xR );…¢"0ËÌ€%½¨³ÀC>‡_ ¨¯0qŒõ»SI<³›è-O¸a’˜©¸Š<=Tæ—§A³')sJÂD™…¥4@öv™5™W<߱̾$×yˈ²AÌÂŽ@ÉMãåÙÔ<NÅJÿ*eð¤Z"•Õvúú1à _p„xç›VR®Â­fr†ÅfÆæ Ë*Óó$gŒ'õI)¤<ÿÎØ¶:ª‘ F$„Nà9WñâÈ$Hvò"’®tÓW“ÿ)îX=IXóÂ&¢gñ\ ækÅ«_&¼ow¬A^cÃ9‘âNÆ &È2ÜÒÒä=v;èˆtÁÿ³qñÀ:€ endstream endobj 128 0 obj 917 endobj 126 0 obj << /Type /Page /Parent 122 0 R /Resources 129 0 R /Contents 127 0 R /MediaBox [0 0 612 792] >> endobj 129 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 131 0 obj << /Length 132 0 R /Filter /FlateDecode >> stream xuË Â0E÷ýŠ»¬‹Æ$¾E,u_º)H‰Q*&µOðïMS)nœY óºg˜1 PësƱXq” gŒÃŠAV®EQÉnŠ0ê¬/ÙÂ.RîIƒãu1`3>@hŒSb«7øQ« tnI2ׯg&ÓZ] !#ˆ"ṓy§7d= `cËš×Ñôû¢IËZ•¶8)sÍÌ}½óÌ»»ª¿MŸºHej;±Ÿ%£Í÷ûŸø`õGD endstream endobj 132 0 obj 195 endobj 130 0 obj << /Type /Page /Parent 122 0 R /Resources 133 0 R /Contents 131 0 R /MediaBox [0 0 612 792] >> endobj 133 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 135 0 obj << /Length 136 0 R /Filter /FlateDecode >> stream x­’_o‚0ÅßùçQ@Zd¨ËÈ2ãÞMÈöb²°Z•Åù£†o¿R–edKfy -åþÎ9÷fX"ƒ«ž;BL)rŽWHŒæ+ô'«o9ÄÕ«9lwúÑ¥xŠ@h]ÌÕo›øtì!=uŠhƒÁâÄ%DªH,‡}Ââ’¯Ç"úÀ"²´¤¶¼®×îz`6!ƒfÕ4Qá1;ÆyÉóxárÈíl6Oi‡[^®§xäoŒËRÝAýÕðþ"¦É§Åÿ$¦Ï¹íù¦m0YÊ>/°ÉS¸+,ª|hé8þ  7ŽqpU`5áC­ó.a;œUþ±Dväy…÷ ‰r/cÁoàÜŸ^¹—¦×Ü/²áû[;4ÿÿ¾c ß¿)¥:z;¼Q¯'ˆŽo$…Ý-äýŸØÉáÚãå'¬Áö( endstream endobj 136 0 obj 332 endobj 134 0 obj << /Type /Page /Parent 122 0 R /Resources 137 0 R /Contents 135 0 R /MediaBox [0 0 612 792] >> endobj 137 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 139 0 obj << /Length 140 0 R /Filter /FlateDecode >> stream x­Umk£0þî¯x>®0Ië¬+Ǖܗî²ã`P\ÌVGÕUíFÿý%ñ¥±èV¨j6ÏkâØÂŸkBázÇ_$¸šç,W·läL>e[å`såŠ?ÚÔ`1~ú TNf«³I :Ãqõkb‰Qø/¸X|ðq*X¿o"<´,kÿ ßP”šéÕ|ÍU †0“©†fH´xÛ]<Ëq‡Gž„Qòzs3O£Äœ½òâéâ#ØìøŠñ¤OÌ@§ÑmE¦ô§ï"Ó§Ü;:%б„|žã%Kcmbþþ eÇ z호Fi>Äñ¹ŽØŸÂÿ Ávdz=ž÷ˆ„ú$ˆùÊï€[….qkdM÷Q ÿ|Ý®VM÷w ’BT@ZoÎ2b:î1¢¥Qn¶  ˆ ¿§5°åÃé÷v‹oj­l çÎ Ìï/-&VØ* /µ«B˜+‡NÇï]]d¬…Ü*™ˆy~¿ôKߊ¶Ž¾Ú¤b“‰ÒšNy3ß=+>2ëËÜ&-í–Ý#–ªp+_Ú,£p_-ôc_T$J4±”l±‹ª‘š[“Òù+‘\woAr)¶°ë¤†ñÀíÚ€ÔF+›9„Ë^WûÞ„™u÷ÊŸ‡e Þ=u+kŸ…Öó¦vWët&+ÿߟ…nv›Z•6î°…’/ʧ2ïüú¢„C8D»:˜f!Ïä{Oé`ÁÃü*ó endstream endobj 140 0 obj 552 endobj 138 0 obj << /Type /Page /Parent 122 0 R /Resources 141 0 R /Contents 139 0 R /MediaBox [0 0 612 792] >> endobj 141 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 143 0 obj << /Length 144 0 R /Filter /FlateDecode >> stream x­Vïo›0ýÎ_qW©@L’æÇÔjZ–J•¦v]ѦI•"׸…ìÄ&Íòßï0š–t™ ‘B0„{÷ÞóÝ­àVÐÃÏ `4 @qø ü™&À´¹ÕÍŠ§<Ò3G¹X_ð½Àa|ÅËzæì’aÁ aþùÀÃUïáÃü‰ È$Fb2[¦ £9<Ï;‚ðæ¡c Õ¯7﫯Ê`°'˜KÈØŠæѲ-|Z­©Ê¹Òp ?¸ˆñ0Îd"ܳžß~x¢éš/9>qÁðöèãLÉO¾ ̾ÌÝþÐcR°caú\ý’Ð&°p»äGŽ¡ã{錞8%ù€Ç&NX äŸ X­¹ÚÂÝÌ^ÐŒwùpòw'z·ŠlåýBÿýy,Xyÿ ÈÑõîYGDŒû{ˆhä ‰v0@:Ðb9°ÁÃáïõ!“±óbghžr–ÃìêâÒc¸ÃItl]åHn±txü½»‹ô-‘&C™gW—áü2ô2ÊâDðE*±È$²‚SÞÔë;ƒ§Ðú¸¶ÉÀR»A·…ˆI#&Ê$ê‚—¡%úK^Œ$&iâ™´±£H»• [­Òûw"9i/AÅVlÄ®”ꆃQ[2…¶pf,OÚÜ÷ˆdVÞYü|ÞØ{*WV> endobj 145 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 147 0 obj << /Length 148 0 R /Filter /FlateDecode >> stream x­WkOâ@ýÞ_q¿­&¶¥ä±Ñì.Ë&&Wmv³‰ )í U:3SYþýÞNLµ¸Š‰Ò¡öž{ιÖp kháϹãBoà'ð(Ø#á@ ÔG-Az—å´Ô+;,¯zø-×bøæã¦k©¿¦ÓuÁí´Á‹Áþѱð¼9œŒ_…˜a¤€Å«eø’„–e‚÷cÏPÊÇ«ç•WY0ØÌtœ¾ÍH£Å[ø²N|. p¿ #ú8ŽXDÍËG"N^üeB¦¡ï¸·ûpú9“ñS†¯³/s³ÝÕÁ¨ÔôX˜>0ç,¿ Ìۮȩ¡è8Á^::½##ðµYDÁ6È¿Oa¾…Ù"Ìžú1i óî`7=[DÖò~%‡Š|Þ=ÍZÞÿC@%Z ¥Þ¼lˆˆ~{•¼!fÅ Hú4Vx8\ã½ÞrœAßxUÙ‚,I ats5±¬°ižiWÉM¿·ºœ¶&rÅd(óèfâ'žûÁ"¢dºdØd"VÀÉ>ÉLáIµ>k€m§£©]¡[C0%nÎKe6ÁKWý5/J•´c©´±£HùI­TéøJtÎë[PZŠ•Ø…RÍpЫk@ªÑ¦Îl‚åAûžÌÂ{ÀÒ·»2ÀÙS¸²às=ža·Uç:ÉÔûós¬“]…–«  Ðâ:ï˜Oi^ûë6Á[çAÆCÂÓ¹§‹t8غ}Çt»oqEqÀ¯|î/—d™MÜ3œ·à‹grƒ8áœÐ` äo°ðé#9ã Y&gÑço½²YÜÄRj5ÁY„ ЇxÀ½Ïx³÷™n_c½ìC?®®ÇC`+BIöÊ— [2;Œ¸ODû»/ý{–ð€Ø¥-„%ÅKù´¾PvhÄäïÝìâC,ìRmWc> endobj 149 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 151 0 obj << /Length 152 0 R /Filter /FlateDecode >> stream x=NË Â0¼÷+æ¨Óݰšæj¨÷BÀs‰õQh¤M~¾Iwü`gt˜A'Ö0VcpEDí#¤-"¤PZJ›rÍÏü+“‡¤«0áìÁ"[\X³²Gk-ÌBªø õEáïØ¹><_ñ>ÞàÞq>ë~¬ZŸß꾈$+ endstream endobj 152 0 obj 129 endobj 150 0 obj << /Type /Page /Parent 122 0 R /Resources 153 0 R /Contents 151 0 R /MediaBox [0 0 612 792] >> endobj 153 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 156 0 obj << /Length 157 0 R /Filter /FlateDecode >> stream x½ Â0…÷>Å;Ô&ñ¿¢ˆ¢“ƒBÐÅ%¶©Vm£Môé­ To†Ë9ɽ_Î+\AŠÓ¡ Ý>C.±Aª)B]^èðõªAIYÖ¬T·$Ì SL8({-#e÷h›µšà)üy«Q¸à1ê ñHÎwq>%ÙFÁ$"aÄNh¸?bÆíÏ*J¹¶R–‰Ì7ѱÄJDI+ĹJ?ˆ.þþ éQÚûN™Þ1¾ä*º…FcˆµÌ¢"m,­çöÒlë[wðλz±Î`Á endstream endobj 157 0 obj 212 endobj 154 0 obj << /Type /Page /Parent 155 0 R /Resources 158 0 R /Contents 156 0 R /MediaBox [0 0 612 792] >> endobj 158 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 160 0 obj << /Length 161 0 R /Filter /FlateDecode >> stream x­’_Oƒ0Åßù'™ ã2‡q1.ú´—}ÙKe êhƒŸÞÔH¢Ñö¡é½m÷œÛ#¶8ÂRóœØð–6*†G”˜¯A(Ú”6§fÄjGìwžºhÙZXà&±›Ç¬v5‰kÃv ÌŠ"ˆ1ÝÐ×4¯!iž¥å’C& •tOó -xÂmÐUÖSÚgû]ÇÄs jq‚ §‘@ZÆqÅ‹OD?މ4 ¹øª²¨qý\ñèJ+<°2Rj}ÿ¾‹™«“»éθü»^sáø^r¤J±…8í“à1/˜L×i^1ÕÈ•1,ú o¨á½Ñ¼ðR— "ûÎp­ý{}OÓaÓ]~¤iÍ·R†Ÿ…œg)·»¤…Ê® ¯ÛƒzcßýíÙ¿ endstream endobj 161 0 obj 313 endobj 159 0 obj << /Type /Page /Parent 155 0 R /Resources 162 0 R /Contents 160 0 R /MediaBox [0 0 612 792] >> endobj 162 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 164 0 obj << /Length 165 0 R /Filter /FlateDecode >> stream x­’_OÂ0Åß÷)N‚ ð0XÁHŒDŸxdÁSº&[ k‰ÁOo×-$lšÞþùÝsîÝa†<3¯‰ÁÐGÎñîD0e<(VÜêÏŽ2Xïæ¡ç;,ÃCâŸyvuIàÃï÷fè>õ;&Š0FkJ?’ôMÓM"VÐzÍQM—TñQÛ ßð–™Õûm½+™8Ã<’ØÀTÒH!±DœËì ±ßωt ¹ù®2;à~›ËhÏ´Âæ\DFíhô\ÆÜñŠëEkѾ½\¯Û ŽøZr¸NŒb µ_*®!c(™q½.\§iÎit@jŒáÑ?XÐs¨ ÝÀ»M ª6?îØÞ«kú— »Áð”æme ¿bRn~ÞnA3s:Fsb/6 û/¯þðÄþJz•‰;Þæ 㯌 ­­ ¨€ŽíïÙ'<åÙu endstream endobj 165 0 obj 339 endobj 163 0 obj << /Type /Page /Parent 155 0 R /Resources 166 0 R /Contents 164 0 R /MediaBox [0 0 612 792] >> endobj 166 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 168 0 obj << /Length 169 0 R /Filter /FlateDecode >> stream x­“MkÂ@†ïù/´ ¢ÙhjµTJ¥=y¨êE(k2jšÕìJ±¿¾›b,,šÂÎnö÷™™-¦ØÂÒï³ÑØH 3$èŒ%ƒ'ó- ÒËNµ™•?E°Zõõ–mx1ž]0;»ÌÊ¿&slؽ.Ü×^[Gá.Ñœðï ÚCñ( ’”€Z|®ø‚K¶ ÷/n‘Y¥’_[­ MœÐ<(…â &‚ûA²X¦">Rlá\ÁS&MÆî»Œ÷xÚ¤ÂßyJâï”øÚípøVÄÌÑŠÔ¼9o=\î×ì:ùʲ»´c ¹[HRKH“ZgÔy”÷÷ˆ4ò¯€ ×?äPú_"i(pþÜÈ{¯ªé*l:ƒºš‘µ•~ë twÂc½;Bcœldø/¯þ †¿´^fbŽ6iàчG‰’ó¦ã”‚ÆEý­Û­f¿*ø,‡½ÛèI¢#ÞØ“º‚MÖëÖ¡ç³\@ÏC’~…vgN xi_Éù‘¢b®ªÂN¿. endstream endobj 169 0 obj 398 endobj 167 0 obj << /Type /Page /Parent 155 0 R /Resources 170 0 R /Contents 168 0 R /MediaBox [0 0 612 792] >> endobj 170 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 172 0 obj << /Length 173 0 R /Filter /FlateDecode >> stream x­”KOÂ@…÷ý'ÁX´v ÁHŒFW.$6º!1c{‘ÚÇ`gˆÁ_ïtÚ”j$Á@»hæÑ9÷|÷ÞùÀ põ{Æ<ŒÆ Â3rœÞH†Pš%2,w9Ì5O5ÙŒFúG׳ ×˜W暯Í|Þp€ ÃéÝÐѳèÝó¯8Ý@ñ4‰ó7(µ$D\ñW.iÒ·‚wÜUdŠ9¶U𨡹U´*Åî$â|!°(DöC±}w™´;ÿí2ÛàjUˆh*‰K> endobj 174 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 176 0 obj << /Length 177 0 R /Filter /FlateDecode >> stream x­”KoÚ@…÷þG¢°0ñ\U­ª¨]EU“ºÍ©ìëàú1Ä3(¡Uÿ{ÆŠD…í…5Ï™óÝÇ#nñK¿o™éÌFN¸G†«…dðe¹dAúÅ®³Ê§šlFSý£e~Š˜]f•_“96ìÉ^Š«Ï“‘ž…bpÃGÉŠ'q”=@ ¨5!àŠ¯¸¤ùÐð~á“WݬQ)mF•&NhîJ±‡Á‰( Â\¤ŠCœ+xÊ¤ÉØõk—é6¹¶¾’x”Úí|þµš3ÝRËÁrøîr¿æØÙË7–½u¤KpÈíJ’‚!EJj]PçIN<Ø!Ñ`(èÁdº¿CèžDÖWà2þp£Ì½&¦ÿaÓ™µÕŒ"­4ð7¾qD§qg<Õ«.ú‹rc¿Àyôg-üµõú&¦»É#Ÿ~ú”)¹8N-h\”ß:ÝZö›€ß—°·]ItÀ;RØd“qzYËôø<ä1ià¤;sZÀkûxL¦PBŠªº:;°§úˆÉ¦³kãU÷êᛊ’™P(YëjFðµ­c¾QˆåàûÝ|¾™¢geº¾HÓ¨ìË!þt@ÓfÇ4¡Ÿ¾ˆ'Dª/±â~ ®’ª ÃöqÏ(”sRÛ<ë¢6íq«Têâü»¿ûí 앦ˆ endstream endobj 177 0 obj 506 endobj 175 0 obj << /Type /Page /Parent 155 0 R /Resources 178 0 R /Contents 176 0 R /MediaBox [0 0 612 792] >> endobj 178 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 180 0 obj << /Length 181 0 R /Filter /FlateDecode >> stream x­–ÝnÚ@…ïýG"pa‚ ª´*j®’¶iÝæ -öŽ–xåÝ»k»`B‘¨°‘@ØÆgæ›33<áOh«×•e£×·‘àr$,x"»Ô†ðô]-«ùÉí·žúaÛ6¼Ÿ\X¶~X;û4-džÝíÀqyÓm©³pghܲßA´dQ$sH¹ øL²)4hî#>»yd[•ì±Ûo¹&Žhî\±†[Î| ™qÌRï)6qªà±$M˺~›e¼ÁÇeÊý•'Þã%¾Êv0ø–Ÿ3‡s’ãÆ¸ùîü|ÍŽ³“ߦì.•±ƒXMIðI.4u¥Äü "†ü t{»ŠB×°æI]‚‰ð_ÀÌ{ÛšþO…M§_V3´­ð ó0 ã¸««CÔGÙuÿüê÷Kø‹Ô‹HÌá2 <šx”H1n8N!hœåoe·RúÛ‚?d°WKÕI´Ç’¤iu;eèY/çÐÃÓ‡¤€W`wË)/ÒWÀC2‡>E$)ï«“ {lŽ˜V¯m¼™^5üA!ákÕM­V«¾vûo0øñóû`0≤gi=ÇA69ÆM¼T@Ó¶iB5|ák².0e^&÷LUEÂöáÌÐÊ)ÉUšÀª¢7íN©UŠæ|E‚¼“rtðÛÎáäÏá”qG©·`jò+9‘Ù&¥˜‰¨¢nW%»]K» Rk|­ß<ž™*E µŒ¦Å¢"³öJ®)¸ju½ötÊsò'|úHùö›§´Ä .&Ê¿ùE¼Vàºd Èá¡Ûèk’9dQô7¼I¾Ë3ãœÍd÷÷ —­&K 7Á³î X§[ïèpê¨?o‡Óëî¹÷pN* endstream endobj 181 0 obj 672 endobj 179 0 obj << /Type /Page /Parent 155 0 R /Resources 182 0 R /Contents 180 0 R /MediaBox [0 0 612 792] >> endobj 182 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 184 0 obj << /Length 185 0 R /Filter /FlateDecode >> stream x­–[oÚ@…ßý+ŽD$àÁUݪ¨yê-­Û¼ ¡ÅÀñe‰wiB£ü÷îÚqâ"QÅFym|f¾93ë\â}õ9³lŒÆ62ÂRœN…_ä—ú¾¾«gõó£XÜŸÔû¶á'øàÁ²õÃúù¯i96ìá^‚Ó‹aO­Â[¢ó‰ý ã$‹£0]ArÈ5!`’-˜ I×ð®ñÑ+"Û«äÝŸš8 ù¤hŠ-|â,Ó%Ç2ãÉ3Å.Ž<”¤iYç/³Lvx¿Éx°õ¥À[ü¢4PÙN&ߊ5Ó]‘œufÝ7¯Ï×8Oòû”½u¨2`Û… ¾„à ɵ¦ÎâŒX°C¬ÀPЂáè)†²Ð-Üò´-ÁDô/àFî½}Mÿ§Â¦3®ªÚV ø‰ÏyÒaÜ)KÔUíi~c[ã}õÇüeêe$¦»ÉBŸæ>¥RÌ:ŽS ¯ò·²[%ý}Á¯rØÛê$zÆ;’ ¤i Uèy/УãG¤€7`wË©/ÓWÀ#2Ý€b’TôÕÑ…=4GLk4>7^L¯~È0Ž‘r‰œµê¦^¯×_»_ç.1ëüü>™Ly*éNš®Ï“$Ì'Ǭ‹ûhÚV&ÔÑÂ~‹P¶ÌÀä3S5‘°]ŸZ9#¹ÍRXMô¦=¨´JÙœ X‚w´C~Û©OþÞTùŸ)ó×LM~%'rÛd”°0MÔí¬b—² ioMj¿Õ_>O…Ì”¢„ÚŒ1%¢!³Ž*®)¹jõrÛÓJs¾¸¦b÷[e´Á=N榦¿YxhÁyÅ@:úQ BeE:ˆ,0®;¬.þ¸¢{ùkÎÅtY?2šûpc/Õ’ ˜.Â;ÝÆ‰š]ïŽOþà„؃ڄ¬@½ü ¦8iÁ endstream endobj 185 0 obj 682 endobj 183 0 obj << /Type /Page /Parent 155 0 R /Resources 186 0 R /Contents 184 0 R /MediaBox [0 0 612 792] >> endobj 186 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 189 0 obj << /Length 190 0 R /Filter /FlateDecode >> stream x­•Ý‹Ú@ÅßóW° >Ä5Ѭ¥R*íCÙ–n›v_„2&WM“̸™kKÿ÷N>ˆqÝ€‹‰ äÜ9¿{îGÜã}ý¹µlŒ&6Â8næÒ‚'³[}H/}ªgõ³#¿Xžôû¶áÅxç²ӗõ³_ÓrlØÃÜ7†=}î ;ö'ˆP, ¾†P‚Ï[2IÓ®áþÂ{7_Y©’½¶<Ë5Q£yT4rÅîó%¾X%">QìâRÁ:“¦eŸºŒx»M„¿ó”Äü îk·Óé—üš9[“ZtÝ××û5ÎQ¾´ìníX‚Aî–’Ä RĤ6)u%Äü" †ü GÇ5…na/x[Éð9àF–½²¦/©°éLªjF+ ü•'DP=nÎb}w†ö<{°â¿¾ú“ þÂz±s¶M~zÄ•\t§4®Ê·Ž[Å~Yð‡ ön«;‰Nxã@ª›ÖpP…žõr=¼ yHxq·œ ð¾’9ó)"Ey_]\غ9bZ£ÉØx2½Zø¦‚( kÝM½^¯¾vÿœo°Â¢óýët:\ÑoeÎ<ÇA69]üm€¦mÓ„>Zø,öT[bɼL„ª ÃöùÌH•R»„Ãj¢7íA¥UŠæüŠ$ix'¤vðÛÎùäÏáÍuñ‰oÃôä×r2‹MB1 ¸l¢n·•¸]K»ÒÛø>ýò—*ÑŠ z3ZFˆÂ:ª¤¦àš«ÜI…µÈCSlî{vÐëÉ–”Ê_½Ã›ö¸Šù“VID¥Ñ}á8¨/öä™,Ctÿ©6.ê endstream endobj 190 0 obj 627 endobj 187 0 obj << /Type /Page /Parent 188 0 R /Resources 191 0 R /Contents 189 0 R /MediaBox [0 0 612 792] >> endobj 191 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 193 0 obj << /Length 194 0 R /Filter /FlateDecode >> stream xeŽÍ Â0„ï}Š9ê¡i6¦?9ªèÙBÀ³ÄˆÚÚ¤ïMUpö°ìÎ.ßLh1/U‘@­œÅŠ£'-oÂuP“–ëT/\d¦ÇAƒ¤Œvè¹ VªEÈ•ªX#¡{gÉ8úÍÅoëæÎz܆;æ§íÜú•tʵ2BÂ/?ñO”厩 9‘䉗ýòöÆXïGçÒ~j®;T endstream endobj 194 0 obj 165 endobj 192 0 obj << /Type /Page /Parent 188 0 R /Resources 195 0 R /Contents 193 0 R /MediaBox [0 0 612 792] >> endobj 195 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 197 0 obj << /Length 198 0 R /Filter /FlateDecode >> stream x­TMOƒ0¾ó+Þ.FɶD=¹Ä%Df!ëFÍ(pÓÿ»¥%PfpK ø(ôù¤;˜Ã,~\!¼‰ )`0¼Ë„™˜² ‹·Ld‰!Vwÿвµ0†[],f‰³F6Ø®~ Ã×äOÁ_ÃÅŒ`vˆè–˜¦ ”Ax&lEÙf:á0¢¬˜hþÜû’a…&–¯î$6´`×ÈšDˆp×7ð:€®‹µ#eÀÇ{š¬>Â\À|g½’÷$gõËó ´9kØÎøØÚ‚@s¤dOÒŒt,¿ éz,æ:l8£šBåq“p‚3Žà§å®WÖ¥*@KeÁ6 qNÖ’AY±Çò­^ŒMjF­Fü—ÅÙ}l­ƒ‡j Š)Í4޳è^±ÒBEya¶!œWîö«Ý«êåG˜(-lUž'‚±¦Ûœ¤½„Ž,¥‡Šö¦å‡ˆðMµØtŽÅ….ÍÑýü )åSô‡ eÁ2ù<A÷ Gi_«2èÞzä*ýSÄŸÌ€#èAüH)`%~¡ìtó_Ÿ¿¢Ö endstream endobj 198 0 obj 429 endobj 196 0 obj << /Type /Page /Parent 188 0 R /Resources 199 0 R /Contents 197 0 R /MediaBox [0 0 612 792] >> endobj 199 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 201 0 obj << /Length 202 0 R /Filter /FlateDecode >> stream x­V]o›0}çW\i•H¤‘bHš¤Û¢m]÷´J«mK…\ãº`·@ú¡ªÿ}¶a`’:M䰉Ͻç_ß[8‡[pÅïy0žzRø O2$SS.dD~5@®ºŠÁêm,þèzIàë'sÕÝA#¼¡ó¿bæWÐ;£˜ÝGñŠˆä…_”…1[ŸaÅLNõ­ù5œÎ‹+4µ|õV`ƒ»F¶ d€gðiúÐvq¡‰µ‘ˆë&åášä æ bu·«ô~³öûý01ëxþd“Z@óJéM3Äapù(S·“‚aBk‚T‡PqÜ A‚™GðÜEæÃq [šJ‚–™+NpsfР´Øò«NˆM눌DìÒbo?í0Fu)M56µho‰æB-sa¶¤\Xî îb¬{¯V¨“0Õ\hÌ<ç*ç*^å4íDtäj>ÔroR~QQTeÑa8QvAŽ}ÑÍn@H3Ÿ–?á1 .ùÃë´wò5÷©ØÒ ½ëÑPóŸ–ü«b :H~¤°Jþâ •Ît‚9hº}Ê$ð¹>eà ,{Îìÿࢷèèâ€AZ^%·ü€pþ7N~º2ù‰úÊ–¡´§Øó5—ËPb±—_ $8çé¢Gx–„²òHî‚—‘fv£çš¬ßøM™*G;)DÞ‘¶ *Ö»Ÿîðj]Ô%„\žkóXª;¬º­·ô^Ž7n˜Ç’Ÿêú`Ñ“ÊñËk)œÔÉ™1úK×.úðÔ…@­0k½ƒo2žÐ\ô˜K ÈÚÝÜ·§7Õl©€¬Þ³¶î+M³q]_ôv›Í¥$­Üá Çã0 ÊqÁ,fë+Lòu*÷…èö¾„vâ1¯^oÛiÍí‡<ø†ºhlv|C ¸ß©ªSø(…ùšõéüfºãÝ endstream endobj 202 0 obj 722 endobj 200 0 obj << /Type /Page /Parent 188 0 R /Resources 203 0 R /Contents 201 0 R /MediaBox [0 0 612 792] >> endobj 203 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 205 0 obj << /Length 206 0 R /Filter /FlateDecode >> stream x…1 Â0…÷üŠ7ê`š 1iVE7‡B°s‰Q«4ÒÖÿI ÅAðÞpܽ{÷õ¨ÐC$i’0Vb¨QìG‚'K`ôùŠK“«œ—ËdRPHæ;ìH©ÉÎ]o9il¬Õ¼TpŠ£âwÅêâ¥7œoc@ó^Ã=pp¨Ø?~°´å6Áˆ”ø¦±™V¿†çòÕª¹7\ endstream endobj 206 0 obj 152 endobj 204 0 obj << /Type /Page /Parent 188 0 R /Resources 207 0 R /Contents 205 0 R /MediaBox [0 0 612 792] >> endobj 207 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 209 0 obj << /Length 210 0 R /Filter /FlateDecode >> stream x­”_Kã@Åßó)lÁŒÎÄþ±ŠaYÑçÂîK Ää¶fq&šIa‹ø%w¿ÐÎLþ²Z)˜éC™ÛöžÓß=7ϸÃ3˜~-¸åÊGIø ‰ÓkÅ‘*ûƒJÍ·N8³§.v·¥þ!óTà[ÎM3fß=>÷áÏBÓÛÙ‰®"\Ãý‚ïÛ ¹„*A’vS„¿pÖnºÎ¶UwûXçìŒÚÞkEe…—Ï÷÷Îß@±‹Ü‰¢Çõñ$¯HÄ2Mq…¯ñåÔ9ôOa<ïŒõªNM¯VÅ$-rW»'Òb?Hf¹Ü\\\ëb¨k^°¡*r\z»4,œƒXïµ5ÓSn‡:°µ•¤4‘Μf1ü¹ß 6)2̱ô½€Ê²(c¡õ“ EîŸð!WIúKBV‚,*$iJOŽz G£é“Z 8@b–TmKi¹–ñ½Ü—3æ´Ü^÷ÞÖçów[‹]“´ûâ·ZÛŽWW"×Àûôîz«Å»ãmÅp+éI–Åöf.rÍ ÄyfcÞÅÎ šòñKÈÙ²·÷ßlÍ|?:M[ƒæIá}qœìq>x8 ÖÁCFk½Y³–ãLŒû«žIÅA ïþÞy endstream endobj 210 0 obj 470 endobj 208 0 obj << /Type /Page /Parent 188 0 R /Resources 211 0 R /Contents 209 0 R /MediaBox [0 0 612 792] >> endobj 211 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 213 0 obj << /Length 214 0 R /Filter /FlateDecode >> stream x­VÑn›0}ÏW\i‘J¤’b7 I«¢jÕ6íaÒ:¡í ¸$lZìH‹¦ýäöC³ ¯(Up*nŸãsϹæá,ñ™ ö‚Bð 2¸z`B¦¾²€…ò¿ÆÄR«,ÖO¶ø¡Ea o] Dnf©¿&™R S Ü®ÞOÆ¢ n Æø¼ådÀò!Í3ÜÀýïÜ’M½³Úª~:Žs}öK°àðëüýÍùˆ•îß=¡ûŠY”d«››QtEÍtVÈ=C²;tn·R‹ÁIZwÒšˆ.ÑÚfd „"59¡EâOiX¹Hê —Rßt°(òÂO~°BÏøã®i®“ !Ê‘A–sŸ8\4‚\üõF}tj¦P“D2,o‹Lé~šÇ;u·'Ö`¯»&ÃïÓíݹõ|Úºuº«œ¶Ì £Ub«öŠŠgHñÎή¹˜µ¶w.÷¦D‘¯ždà†#%í §¼/{ú_Âe°Y/'¦Z0´)qË&b9K˜~‰~Qó£’†é„}i8_1óŒ’t?á¥×Z’´ðž>V;Í%Þ½Z‡úÐ77Èâ\œ.\WmèEóÙa‚儬BóBw…xæÛµµ·ƒJAm=þ”©‰ endstream endobj 214 0 obj 671 endobj 212 0 obj << /Type /Page /Parent 188 0 R /Resources 215 0 R /Contents 213 0 R /MediaBox [0 0 612 792] >> endobj 215 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 217 0 obj << /Length 218 0 R /Filter /FlateDecode >> stream x­XkoÛ6ýî_q‡°X©õð³«a4hb†ÞŠY¢-™’Rò'“?´KêiËJ“™ÊY²Ãû:çÜKþ€¯ð†ø76-˜Ì,྅w—„@¨¯† ù« s¨®ìeù4ÁZ †+0-¹ØPÝ sd娰ŠáÝgçßÂjýé“ä*¢[ØpÃ_„†ø0Ÿÿîø–\\\œwVÿÀ§Uæ`iL­^>e¦!7mæ¦ñ®Lã½°ÜÉ,‹t k´þï9œº¸ÉèËç^ñƒÛï ²Û –;øIĨGý˜÷>ÀÒ{ÿzÓEJã2lLòQÓШ ›VþÔb'˜`M@›EšLð†q‚ŒAÔAá¦n°¾óÅø’Bo!”ì®b³#Ù,Ý(.éÆ-RÙ«€g„ö 2·/é•Çz0hÚMÂŽÉ»tN¢PB‘K•§Æ¢|ç'Œg xµ:´RÔtŽsT:¢AÌ‘]/„’U¹tq±Ê0ËøŒE*肽9>ÎùÂ…hƒ@üEY¯¡ïrÞžÓÉèÅ ¥å0"ðôô°±4¶®"ų•3§5é©Q½ùñõ•mëY†5œ¶²]Ú9Tx#¯°v‘xJ»¨T¶ó³¶Ýî‡9;Hvgꪼ‡Îü CMµ¶ì=¥i\¦BÕúo–â‡(K k~@¨l9 : –º[ÎËÊ¢¥îcûźKFg­9]ŽKßH’rJÂ/¨kˆl ñäD¥ú-N"’üzäÕš¼ÌweÊX„dG’|®8]F­Yqµf/Ë/¯›T\íƒ"M:o' ÿX±ì§Ãq+Ël³ígû×Ä ®|º%pív ñH¼äŠpÒ ó9س_i˜äl«Æûƒf[κƢæªDÝÞª¶SS„šczê0¶Ž²/S¹åÛD­urµ'í]û V²ªpç ð)û€ÜÈ9VGI§íÌ­ÌÆ‚³Ýní׺¦ {ÖÞ±ó áœq÷GÂߢl<]²tÒ^7œøXÊ‘ç.ȺËg=xr†5i©áIÊ W²ªƒàŽ99 ¬G ;AÊ¡ücQ¦!æ~ȱj<ƒO·„?d;í;ƯIXÎ(ÿ6pìšF(‹Õl°¨€Åq”œ§wgT“€ƒHs8‰ÄORQÃÓ¿éÂ˸}Èð²ßˆÞ€žÖ‰×™ßE¼aàh_{Ú#뎆MΟÁŠªÆdゟ«Á `!Ñ p#«Ù§»ùÎ7xª¹ý^ˆòæóžÔÓÁ8²›´Ã!mÉI@¢[V[`> endobj 219 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 222 0 obj << /Length 223 0 R /Filter /FlateDecode >> stream x=‹= ƒ0„wÅ3¶CcÞŒ®•vn ÐYR;¢hüÿÔXèÝpÜׂgAïlÄà:Ã:òd¢î³óQir,+e\Aû ÿÎíGmª˜¸ÄÚ£.*U®á"bµj-!Qß­ÒáÍ©ŸS¦aÛÆu:>ܾò_·Ô"r endstream endobj 223 0 obj 122 endobj 220 0 obj << /Type /Page /Parent 221 0 R /Resources 224 0 R /Contents 222 0 R /MediaBox [0 0 612 792] >> endobj 224 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 226 0 obj << /Length 227 0 R /Filter /FlateDecode >> stream x­”_oÚ0Åßó)ÎÃ$@j 6”?™@Óªíuê„6MŠ„Œ1•$4vÔMS¿dû…f'Y”vJxˆb ¿{ï9çÞã÷pôgH(FŠXà;Bôn$—éW$7¿ê'½²Ãâi¤ÿèP‹ø8¡æeNz·É5ô1Ðû<èêSÌ×h‰dËÀCß! }%Ýæ?ñinʱ^¡t†™3…›(X¸ê}IÔ>QJÄÝ}б Ø…®ê`6!ã¢-+kkÏøÛ|áÊ7®›£]·`¿/µy\7O›öä| ‰*ö¹jп®<°8ÔmÉ&ƒQ%"Ÿ\„QIŸ|J|Ǥ|IüiÀã’<¹1 /_.ØRkĸÂtrõz'd´Î’eOÎ…ÊP†Ð:µaëÒJ#_Ä7Íkñ”…¹ÞöΑr–‰³a®"žB‹÷,¡Â:Š¡¶Æ6ú¦Ó¥)„¯D O"}( Ý·˜ ø.J› DÔ;ÙJEÔ¬FŒE&Ï4#Û³ÅF¨ìl¡¢E&¡×ö:o˜mí6#“ó CiãÆ·0ý›rൊó:¤•:Õq5%ìc_;øéG”@ÀÅÓUZ‰= Y ®ž=/|nb”T+ÿøzWÕN—ÒS¥³¸ÆB%qÒHýýRìòUÛHíÃ’4ù{ËßþÏ7÷ endstream endobj 227 0 obj 567 endobj 225 0 obj << /Type /Page /Parent 221 0 R /Resources 228 0 R /Contents 226 0 R /MediaBox [0 0 612 792] >> endobj 228 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 230 0 obj << /Length 231 0 R /Filter /FlateDecode >> stream xµW]o›0}çW܇Jm¤Ðb ÍÇÔhZµ½N­¢M“"E„8-k€uÓÔ?Ùþ¡^c NÀ¥Ù¼ä!ÂI8÷ëÜs¸‡+¸ßçÄ…áØ…ŒÂwHàì’Yñ•,¿:%Nñ’‡ÕÕÿè¸VçWÜÌ)>m2pÁõ=˜ÅpöÅ?ÅS˜­áäšæ,Xn(<¤ÙäIÄÙ¤³Ÿðy&±Þ Pf aÀá2ã Y}Íù6çœf§Û¸gU`YéÀlBFUZ–Lk„wÁ …o4YEÉÍdRBO&ö%Íd]=m׫‘Ë‚æŒãYrÞ à!ÈL‹™€ð‡­eåL •þ”U 7couþŒ‘Òžr0_[KìQr¸˜é¿ä Z fÙãf£$”@8ÞÃãWH« |Eß‚¯Õ•$³~ìÎY‚Îs•†h•ã6ÈhÂafÀoÅØà²1-X§1ž¤xH§7;6PyBÆõT•¥<`–ôIûÞÈÚÛa,_ýEÜS›zyu°1¬¶-‰«²½™ño8bt³† `·Ñz‡à¹(mr¾ÓDÜÉVÑDÄú(š±í¹ÈötqC¹<[ðt![8?™÷¨­v›‘q“¨b pphÞ‚È_„ó“àæ=#luvºŠ¶Y„üô#Í 0§~‰=M‚˜öŸçóäÙD\ÒÞùÇ÷O•¶º®»ßiI׌ò> endobj 232 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 234 0 obj << /Length 235 0 R /Filter /FlateDecode >> stream x­’Áo‚0Æïüß²‹Š83[²£ Ù.&¤«uvNÛ2ýóWÁI¢˜lÑöм×öûµï}̰A`ç '!Ç$ü©&`ºÞ  ÙþTŸõh’Ç(¶ƒÐa%3p/Ô«G†!Âh€¬„ÿõmÙ½© •ì«,©\x…B®–”ñÔu²O> endobj 236 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 238 0 obj << /Length 239 0 R /Filter /FlateDecode >> stream x­VKÚ0¼çW|U/p0yËÂJHíª•zÜ.Ú^VBÆ1‹»‰¶»ÿ¾_2(”À%!3Ï|à àá÷Î`6@qøÜGíÓù%4Ëþ5ò½üSœ´G3¼Ñ Ã÷øAæå¿ÄŸLưŠÁý9áYXmaðKjC¥–Ä1•!‰„ä ¤ájK_ Õ_ø±*žÌ²ä°ö¨à„Î3£S0.QG.Ã!t'¾ïT}ýâ¦Z¹!Ý=WÑM$NÓª‘ñô¾ºl©æ Ì<\OÐf ™Ì NTI!ßt3¿‘âm@Š>æõU*Ñ‹Ç2]KþÁYjøú$Ìn­w<ŠÖ{ªh¬×¾5ÿæuð:|è;LÆü¬ØFoäö¼q=y¥J ÛDA©½‡tø“ñYI9¶›ô³ö;†4A3†Ç{&7n€‚F—è&ÂZÀ }83½HJé ` I7©’×ëj(BRT{áwŽ 4аß4W†‡9©† eï™\³ãÀRm’˜«ëŸ¡uªý»úX„"æÀ ØXå¨ Ë(²/Å”í°Ôû0nV)”`÷}Øx½aç,æ2½>mfeØU³žwÉ)Ÿ¡¬'4Ð#Eo`»¶U¾}!Ѷ ¥`ïü†÷|Cò™jB?»”“ÔØyD‚à¢&¬M‡”bCõ0V|Õ)«ªd©Ëê>WÁø¢,¬WØPGÁz(À&øç§6Ä"/¶ÚfÝùß®¹!ëeì¦ÕŠpK°Àðô¨K²´ endstream endobj 239 0 obj 643 endobj 237 0 obj << /Type /Page /Parent 221 0 R /Resources 240 0 R /Contents 238 0 R /MediaBox [0 0 612 792] >> endobj 240 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 242 0 obj << /Length 243 0 R /Filter /FlateDecode >> stream xµX]oÚ@|çWlÕ<$ÛÄ)A©©­šµª æ.ö™øÎ$ù÷]û®Æ²zWò€øÈÌíììxÍ Üà ¸ø7ð|Ž|È(ü½ ÷ äåG.ð°øV×sˇ|³z5ÄtýN˜ÀÍ<¿sËgÇ |ðÏû0M ÷鼋ïÂt §wŒ „i’¶pâˆQˆ˜ Ù’„ôò¬3ý·Sy²Š¥„­^INØÃ¹cìHÆk‰€-e‹30w<ï¢Ó¨èã‡^γÞW~¿U¯0¡¶Qh!uð§vA.W^y½ó·­Yãue» Ók«„øS,ƒöHU,GO­~“õ‡íú:þr[‰ô/·A0G‡üy|sûÙÚó=ö·‡»‰Ù±•êµéS6uðatl©Èd3h¥ÈÁVŠŒÚƒK,È¡¤é:2 ¥‡üÄí£sK=úxÙ¼ý›á–öÐÁŽ7›Ø†:ì¯ëºFÇVrøµ­B Lh„+åÐÀNÒµ54ШF`E Ío?ÓVd[¬àê6£Ôx K\”Í7†~ý÷‚?·¶»Üÿ[% ž endstream endobj 243 0 obj 846 endobj 241 0 obj << /Type /Page /Parent 221 0 R /Resources 244 0 R /Contents 242 0 R /MediaBox [0 0 612 792] >> endobj 244 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 246 0 obj << /Length 247 0 R /Filter /FlateDecode >> stream xµXQoÚ0~ϯ¸i}hIh P ‰¢NªÔMk‹6Mª„L0%#qhìÐößï’X!JŒ³¾Ïw÷Ý—³_á^ÁÁWßõ`0ô ¥ðt§Ü…€·àAþ«ŽëWùeõi€t<+ˆáf®—ƒ9Å»íúx—=˜ÅÐýrÙÁoa¶‚ó;Æa‚$Ž [ÚQÈ(„LÐtE:º°f¿ávV®¬b)`«O%'àÜ3Z%ã"`GÙòtÁm×½²}þÔÍxÚ]„¬»¥it‰¥ÊšÝó¯šiË8.Ò0×Ç*‹}9P¼‘”…ì…› ¸JŠX¤0Á0lgI¢FS©®1}§A&èü-ë9_Ó(šoIJb>GñÍé{(žÏŸ/®õe‡Êî#®¤×éš^¯­<%‡U’‚ŒÝ€:ÜËÞ>Ù¶‹ìC {ˆc« 'BÐx+@$ðBàX%²ˆÐð†‰Êø5¥ÈÊPÈ씊,eÇÇ¥02.¤húÂc $ŠÐß8M]¤$ØäáŠ5… ã"‰izüvµÛo·5À2Œ©x ­£Â‚幇—Å$X£©›(Ü ÝRP‚ò8!_k†èÍšíC*HÚAý9\¥aÛA*ÄŽFM´™‚£ %ÌΟÁK’–‚ñ±5Q.Ï©ùFÕg1eÙñú;T¬»Y¬§uòVôPîÈŽ„¥o uݪ_lÏ­9Gå„, 6ô„ç¼B EO©Ð÷U*HÚÔžlÏ«ÙDU¦×Œ Ch«¾Y©**ÉÒK¿¯¼^Í,ªZ¡CíÂÀ€ªàŸJpìÚ%y9rðJëÖߦf…Ö¥ìü¦EXçc(G ö'Yúí–ªXNîZõ$ë Ú ômòõ¶JÒ¿l-|¶ ù~rs{¯ízý¾ûûãÝToÙ2ëWµî“2µñÒZv™‘ÈzÐ2#ÿ[fdØn\b  Øi’lB=3ùP€Ÿ9¾¯µî2=Üc6· -ÜB*ØÉv™H‡ ûÌí8ŽÖ²e:¼ÚT!&ÐÂ-Ó¡€&#ÙP@c6|#ÙPœ üJ2X“]>‚ËmF‘{à ¬pPÖŸzõó‚ÆÖVgeµŒðè£my|¸3üèS ^RSŠÓ‰1ìEõðï?<ç endstream endobj 247 0 obj 874 endobj 245 0 obj << /Type /Page /Parent 221 0 R /Resources 248 0 R /Contents 246 0 R /MediaBox [0 0 612 792] >> endobj 248 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 250 0 obj << /Length 251 0 R /Filter /FlateDecode >> stream x­’½nÂ0…w?Å@j vHCR•¡´Ý©]ªÈqi*lÇ‘xüæB•.`–}íûs¯÷Xb·œ÷”!ŒÀ;&‹œ‚çuÈEΫ[cêÖ£9lwAùÐe„K<Å ¬JæÖ«C}6õKL^§ãòñ'†«\À~ ‰L!ÁÑ[“È;X­£âoò7ªZB²Ý5<\àui¤¢%m-Tš©íC•W¥w˜7#=?ƒTïv‰Áã‰E -e¢Ò(z®CΜ‘X±nF]äÿš^òèxþ¬_Ô£g.‚'¹Ú¯žQ¤é߀ëL}a £þ4݉ßÚy@ÏrŽß©+çæöÃN¥öWoU•[º+Ù^_—¿xæÌb endstream endobj 251 0 obj 286 endobj 249 0 obj << /Type /Page /Parent 221 0 R /Resources 252 0 R /Contents 250 0 R /MediaBox [0 0 612 792] >> endobj 252 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 255 0 obj << /Length 256 0 R /Filter /FlateDecode >> stream xuAOà †ïýïÍí0[zuÑă1KšxÙ…TÔn-t…èß:»vqû8ò¼Glq +cyÁÑk¼Á`½q •F®Š·ÏcÉÓá¹ËÃCÊ“ªÅC &Ä0Ž;OsRdXÉ{N¤@Ùbý$CùÅkÛ}©Æºƒî\í–(÷x,cœ„–ÊPÅhìN üÉœ„ô+N¹ŒÀ ”DÐna{B6ÖxÝvòµù„5úÎÁþõ­›Ýr– ¨ŽÌø5SwS5ü Ic„”e—×1³ñ½u®|mM@&géäòŠtQˆ€¤,@3*4™¼/ /Ú«wåÕ\qû lÄv endstream endobj 256 0 obj 259 endobj 253 0 obj << /Type /Page /Parent 254 0 R /Resources 257 0 R /Contents 255 0 R /MediaBox [0 0 612 792] >> endobj 257 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 259 0 obj << /Length 260 0 R /Filter /FlateDecode >> stream xQ±NÃ0Ýóo@¢êØN›(Fí@ŒPd`©Tרš8­ÃпÇNDZ) °‡ÓïÞ»÷|ÀPwÆ‘fG7Tˆ–AÙö‰Â*ßEmOWì³Ô R¨·Œy0ÚÆ1›rð)…,ÝOˆ«Bn1ZìskµEî¸BÈîd·FÙbôٯɄþ„_kSïõ9zpZ 'HyFbÄIœæ4¥>r¸]¶pÜwþ%C±Œ:ôNlp)æ}§UcÑC9oõ?Ž``h<ð³<áª>šÍ§jÖ¥nrÌðò,ÄcK,„<Õz> endobj 261 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 262 0 obj << /Type /ExtGState /AAPL:AA true >> endobj 263 0 obj << /Type /ExtGState /AAPL:AA false >> endobj 265 0 obj << /Length 266 0 R /Filter /FlateDecode >> stream xµVQš@~çWLÒ&ÕDEO”FÓöÒöñz m_L]V‹,»ä¿ï삊´ÚZ}À…agæû¾ùÖ'¸‡'°ñ;#¸ r?€ÃøV B?²APe[ª›‡•‹/ÚŽAøà!j3[_Mrã€scƒŸÀøÓÔ»ào`p» …`BÌ5ÿ>úU‡-õ‡Uo‚ÙÔîÚ?ci¶cÇÇŸ±—­2™ë,¬ LÖ [!Ø“«®Žc`-ÀHGEþ©Yh7KlܽjÖh6 éÏGF¥™¦–e«:/‡ÑtÒÂ3)áu–§QAe0¾}õ¼;Øóü2cæjËäz@ò˼ùÎxó­ç}©Þ^ßsùèÀ@n’űB£büÕIA˜-c¹,Ш‚"ˆ£`úQ 4å’qHl@ݸBm»£6•~IÈ‹MHe‘³\å2 ˜]¨ß—ª¶˜b“ôûÆÂœâ ¼˜ äñ]¸Û{.k0b•å)ÃæªŽ«ÀR¤‹õà:ôÍ»„¾êNÀTš7Κñ^M¹8é{´E9‚G?Í—ÑeXæ¼íXŠΞÏb&ŠsœwúÈ!¶ÛÄß• Ø~|”bžÄü屄Mž&€‚ —KÐ.ÀD*ã÷ÞÞK !óc9ÚݪÎÌ¢= Ýæ” ™—û R»W-ä× ™v {ZäPQÔÇ_uhÝC'èoq(YP1Ô ´Çºù¼‰Æ\Ùíšë÷-UŒ”â¼ÝïÅRÙ_ã3Î;ÆÚÿ L2oÌtC;ÊÙúÑ’í,D q-¢êlë²Âû_¢ÿOÔ endstream endobj 266 0 obj 637 endobj 264 0 obj << /Type /Page /Parent 254 0 R /Resources 267 0 R /Contents 265 0 R /MediaBox [0 0 612 792] >> endobj 267 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 269 0 obj << /Length 270 0 R /Filter /FlateDecode >> stream xµVÑn›0}ç+®´II¥b“†$S«uÕ¶Ç®S¶½DB8©+0)8«ò›ëíÚ …liËÚ‡Äàø^Ÿsî¹÷nàü Þ”BÊáH8½Ê™yå@è]C☿üa¹òð‡µ‚>Ì}˜c>mrFž90áôÓhˆOa¾„þUIJŒgÀ0Ö ÌïàãZ00„ÛdZeh匿ÙK£­yª¶%4:¡D8¼†yåëEHÅ¥ò^@?è 7×iÉM‡@ÌäfɵIyªcgÊ0z¦¿o¤Ò¹‰÷q<ƒÅCea°PžÔòøžE‘¿ã²C`©œï3l_ûr°4éÙ¢ß }ãšÀžCßu{`jÍ[GÕøAMyXé;´Ê¤*Ž€É0ççYõeµ–=i:–fFò‡£˜ EŠõ®·þrˆãµq-£-¨[¾+­˜BÞòT(X¦I (X£ãJÆxVƒÊú»·¤…I•Žqw«¯±*"ópC»†eÀ3•nw›¶Æ½ !¿^3dÔVìÉ&…œ¢Ãp¼°Õ¡ýµµˆÑßSÜÏð še]_G£WöÚêú²¡Š’H pÐýž,µýÕÚ˜u\kÎ6™Ôjº¦íl5ýÉŒ¡†¤QÞÛ:µBJkò.mçRc£D°‰´ëe:°÷úZa½TIÔ­5Ñbl*Ú|å*-ò©›Ž¿LRÓëýžnq½nú×D^‚2G÷1 €P=í”bÁ-6u•t†GŸØ ô±£•é¹§ľÈE o«Ûá~/ò±‹¡†Nšå£ ©TW°ÑÌF+#oί™uóÙÊušÍ©‰)f&§ˆÇ®€pɸ⢠¿=ƒ’šà@k}°¨7Øã²~ ƒ6W2€ì€8³‰Ì ”øòˆ¯Ð‰q¼ÄU²V"‘,2‹€Eº¦ñ6¸Òqoþ"¡hú endstream endobj 270 0 obj 869 endobj 268 0 obj << /Type /Page /Parent 254 0 R /Resources 271 0 R /Contents 269 0 R /MediaBox [0 0 612 792] >> endobj 271 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 273 0 obj << /Length 274 0 R /Filter /FlateDecode >> stream xµWÛnã6}×W °â–£‹×нˆÑí¢ÛǽÀÛ¾X‰N¸)G¢ø7»?ÔRÚ’['Q“‡MΜ3çÌä¾À#xø;óˆæþ 7J’R¿ò Lh×Ä÷ôyج"ü¢8É~YïÓažþtý·o=Xmáæãt‚OaµÑ‡Œ•%/á]×°ú¿®LÍ‘úŒfuö‚ÙÔë;Çó]ÆÛ“o~Ã\îKðÃDÁ|B8ŸÌ0sŠè3Œe¸3 ÿ•,t“õ=<Ý$ëØÉBþçwž¨TžO&“6ªËîp:€†<·øiWäé>Qñ–+wðíëbñI_¼X¬;î.ï¹ZB>–lËán W¿s™ y¿X|6ß^_¿»v.å£M¸ëÏÛÃø›£€ð¶/Ô¡†JA¤cÀ4ô«˜I.—*V˜= ¶Ð뉮ÖÉý†%j_ð‚î.Uœàí%ý½—Šb îã*y‹çdáNQ('º@fY×\V`”ÊÝ1Ãî²ÚgÀ"ÒËõhúfV=‡¾#êŽÀ¤šw.ÒøÙšŠPé5ZMP-GÀdjøy–¾œ>Ãro»ŽEÌHþt3©(Pï´õ Ç÷¢ >Éìê×ò¡Šyâ ä/„‚M‘o vè¸Rv^ZP9ÿîígiñýÛ6íîΈ°ªnæi C Ë„—ª8Ô›Ú½ªB~}ÍøÓ>±çû Eçáxa«CûëµØ¢¿ ¦xlˆ+º²¶ßÛh àÊQŸ®ßwªbL…$Š t¿“%ÙŸÕÆœËÚXw.pý[KÓVí³Yõ£Kö<`¬®!ÓÚu °ª»q÷É>cˆRÝÊèb×´zKW/-¤ ´zh55U]¾5•žê±='Þä…nõëÑu¸«aÚC0³j¼e…棡®p²SŠ%ØÓU>Qpâ20ÂfŒNFcO ˆ»4E@ÙR7Ãßëµü1ÄLÜvÕC„´UW±Ñ†*Ãôæ×Œºf´ ½noêG"eŠéÁÉñc( BÖrQÑßACM‡Žp¬6Xéã ¶¸2‡¿„F}+Ã6@ÙD©çIüHyÆïшqºÄU¾S"—,Ó‹„e¤i|Gþözà ñ?ŒîÌò1ÇöSqc £vl’ã£Aù+ß`üèp–ˆ^ÚªÃÈÒ­e·Çœ…òAìôœÙ–´»,êL/zgÅñRk ç–’ê¾Iñ¸G Á8TÁPÚ'H|®t¾X˜ð3õ¬þSU2Í.{ŠI˜DmãQÍvö–iù\ùúª endstream endobj 274 0 obj 990 endobj 272 0 obj << /Type /Page /Parent 254 0 R /Resources 275 0 R /Contents 273 0 R /MediaBox [0 0 612 792] >> endobj 275 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 277 0 obj << /Length 278 0 R /Filter /FlateDecode >> stream x­“]OÂ0…ï÷+Î… ¸Ñ™(D¯251$Ë>*Žlºz1½ÝN3×],{ûqžóv‹¶ êéQý Áð Žö$¡ð“|Š ñ³U%ùØ¿¾új#15?ÆØ¥Ùa$ë´kÂìØ1Úw†ªÂ~Es*°dòÅBÁ|¥†a´`¯pkc¦Ó­NE*Nq½ë 2d †xœ[ÖÔ[)MËzØM¤úHá,šÅºÔánÌ0¡!Ó s ±h]µ´î¨‡àt:(ñ´ ïl/û7œ¹IR’=1„|iY“uÈç5ÐuH…§Žý/”|õ:×5+lY°!—N!}4ÜÀ•®“QåÁÞsÉ–LÔì ÷+žç '­Òx¶â(šNº^ÄJ߯7óJ]-» ÿè6zy€êãdÓüuôóït/?-›} Q endstream endobj 278 0 obj 339 endobj 276 0 obj << /Type /Page /Parent 254 0 R /Resources 279 0 R /Contents 277 0 R /MediaBox [0 0 612 792] >> endobj 279 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 281 0 obj << /Length 282 0 R /Filter /FlateDecode >> stream x­•Qo›0…ßù÷¡R©¡ØaajµµÚ¤½¬MD6©ª„qS*0ÔvدŸ1¦ÐB»LrçÜûùð –ïNol‰ñHØDŒ„"ÎMÓƒ÷ß=X}µ £Ö´U*ÉákÆÒŒ0°ZºîÍúIÖtÝÛr!Ÿ\J9÷§ú½Ü§ABàâF"ψmF÷ã/cC‹ë5¢KÜÍkyFé„”wR•=,.ŒÎke¿ ÝDtëº×iDGg¨›Z-uðÞ£ú„Zß°ÎÙ¸¥­6¢Â×¥{ÃÝ"ð U*ØŸT-a;Ÿí•·˜¯bk5ž'uè¦Á:&µoWß–J”£…#º }îPõ÷ݦ…iü’Цº»,C¶äD…= «%Dd¨,å™Ð(¥A|"M›pèu¤ DäìåÃÉ S_ȳ ”Ý¡ñK@Ýë”F«2–! Ðàá ø ‘ªÚf¦y%åÿ›­FÓj.÷Œû‚ö‘taä´:£SXæj×8‹iƒÌÚ‹&™ËL*ÈíÄrûvÂûã(>MÑ7r‡8ž(xÖžÙÎdôBeZÍ€†„ –WÊ•»|= ;íâ°Â7 ´øž'Ü endstream endobj 282 0 obj 531 endobj 280 0 obj << /Type /Page /Parent 254 0 R /Resources 283 0 R /Contents 281 0 R /MediaBox [0 0 612 792] >> endobj 283 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 285 0 obj << /Length 286 0 R /Filter /FlateDecode >> stream x­”Ko‚@…÷üŠ»0“‚ ¾*FÒ¨mÒ… •Ö ÁaT \øï; È š´àBá"÷»çžÃp…~úH…ÁP…À|hO#8b%"œÜ%#…éÅül@ÿ¨¨ö`bBÉÃö-¡ž jOÃöGW¦WÁظ>ÚÅàpàØ…'Ër Œ#¼°žõ‚Š^_­¼+4°kE‘鑨‚1|ßvü½¦Í-|p|"é{›é-ÁöHp¼7­QKÈXžÎ]Å"¡!§ÒÁšØÚº„‚°$ÝvBÚÙd5FšBÜy Q%¾ÔQ8D¦>‡øZjÚÌŠ­Up 1Ñ´ål2_iš‘02e6b Jt©'nÈ” å#Þ·¨!Æú½<¼”{ÎÂc?Vr.INSÏò]Þ„/ÿ •·o¹naõQ]»GÊ€Cd“&<…Á‰„ñõ‰;'ÞÍ]šÉF6bÓKób:v³‰zåx\£Ü‹ç'ݱHzê‚´ÌØG7üñí$!µSnNñ("S†Z_PP·×&å¥ Î_£‚z…ä2q1 ÿ‘­$nio…rLýr~KL‰;óçâ‘<§– endstream endobj 286 0 obj 462 endobj 284 0 obj << /Type /Page /Parent 254 0 R /Resources 287 0 R /Contents 285 0 R /MediaBox [0 0 612 792] >> endobj 287 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 290 0 obj << /Length 291 0 R /Filter /FlateDecode >> stream x­T]o›0}çW©“H¤@±›†©‘¶©•úÒ­Û– Q¸I¨’Æ´UTí¿Ï6è(Rª`î¹çãú·x€#¯!ã9¶„_HpúE0„B¿r Bõ•ͽ²Íâi$t¸®ðÙcª˜£ïpðo…Ó«¾-wáÍйY?Ñá2‚Ö Òa¶ÜÙ¶Ý…wo\zYW‚.Yñ'{[ÕPp¨$eRÂü´Ù,ÉlaæY¿>ôå±bMÂ-©<Óʼ(¿xK¤‡õy\íðAÈ4ø =ûúHÀªÍ)vö‘Ê…Pí¾ýÖMŒY endstream endobj 291 0 obj 477 endobj 288 0 obj << /Type /Page /Parent 289 0 R /Resources 292 0 R /Contents 290 0 R /MediaBox [0 0 612 792] >> endobj 292 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 294 0 obj << /Length 295 0 R /Filter /FlateDecode >> stream x¥—OsÚ0Åï|Š=¶\ÿÃØÇÒ¦¤¦ga ¬"K‰dÂðí»ÆØc;Èd‡$O»ï§}Ò+üW0ñ˳l6( sðe¤-õþ#t˜ý–a³—Ÿÿ°|âšv/Làë ,×Ýœ½\Óp<è[Žmø.ÌøòÍ5L°`¶„O³˜ˆµ6>ÃìÜÏòu–éZž7<(O­ ¦ ‚ °ÐP̯kü}†-ѰØ0žÂbiLAËeº%XfDß(—/ )¤”$@ð=¦Ÿ{—.ÎKacŦïîWÓ;®xNtÌÄ*•þ öF•fé¾S! #\UÀ%HʤР—ez™EG2Ǫ§¼?˱mÿ¨9½Ü–‚Úàd!I¥ÚÙ¤2Ù+~¾ÓLƒÞé”&–R•ËÙѵœ ,ˈ"u·b¶Šûi¬äf¿lRXea!2rj‰ãã+(9<µ÷À2iì[ƒÀ3¯W㦡LS˜&,kÕ½+Ô;Æ]? ‘ÅÀ«—3Eð@_8­·±C¨¡•˜5a+x’1¿¼ .ÛD~tš ê!˜5aZtζ‹#¡©ð• ø½ j½¤üŠš:¥«‘†GúÆŒBM/ï*õjÓ¯tÉv½Fî"œ9ãÍšêËu:]òÌF~ˆHÁ(‡1©¤òqòQŸ†î …OSœoð“J±º¼¤N“ü ãálËÞG£HÇ‘y•R«IŽéd8œíÙG$oº%BKq¹P—KŽå7ÒO¡»Å‡Ñ‘Ò ›Éqì “î8 )Œ t²4ù Žëe0œ™4gœ3D|âH§Ñ5Nu6O*MNÝ+ˆµ¾¼¬Ná ‘ˆiL¶9äDE¥ÔG¶“åùcòd*á+œú¹8ŠˆÂŒ ½mðUby:Ù5!È8Ì __ãTÃà«”Zâé.ÙÁÙ®™¨ÆQ~œè °ÁªR¨-Ÿ¾“…)eð¨u´oòª-¢&$¡æ”-øu¹Q?5WUµ$T®tÏ#E+þÞí_—Qm õ+çÙ–pR²¯×O¥ªªZJ¬ÿ _7i£RÊCê ôÇÝF¬°¦ä¨ ‹ÆßÉi¯’É3ªÜ¼‡ë Ö‚Éb F·ž÷J©¶šltš¯ärÉ*ún⼞S…O£Xáà(|»¶'·Ãªª<§Î|šP•]Œž(ÃÉ~ÅPï"ýUgf=gÄý8{ߢk"9-!•¿5Þ¸DåÓMûÉ©ÅTáÓ˜¼±ÆRr¼rÞh”kÆ”˜ÙÑą̊ê¿ÿù‡•±ý endstream endobj 295 0 obj 1006 endobj 293 0 obj << /Type /Page /Parent 289 0 R /Resources 296 0 R /Contents 294 0 R /MediaBox [0 0 612 792] >> endobj 296 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 3 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 2 0 R 11 0 R 18 0 R 24 0 R 30 0 R 36 0 R 40 0 R 45 0 R ] >> endobj 50 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 49 0 R 54 0 R 58 0 R 62 0 R 68 0 R 72 0 R 76 0 R 80 0 R ] >> endobj 85 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 84 0 R 91 0 R 95 0 R 101 0 R 105 0 R 109 0 R 113 0 R 117 0 R ] >> endobj 122 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 121 0 R 126 0 R 130 0 R 134 0 R 138 0 R 142 0 R 146 0 R 150 0 R ] >> endobj 155 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 154 0 R 159 0 R 163 0 R 167 0 R 171 0 R 175 0 R 179 0 R 183 0 R ] >> endobj 188 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 187 0 R 192 0 R 196 0 R 200 0 R 204 0 R 208 0 R 212 0 R 216 0 R ] >> endobj 221 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 220 0 R 225 0 R 229 0 R 233 0 R 237 0 R 241 0 R 245 0 R 249 0 R ] >> endobj 254 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 253 0 R 258 0 R 264 0 R 268 0 R 272 0 R 276 0 R 280 0 R 284 0 R ] >> endobj 289 0 obj << /Type /Pages /Parent 298 0 R /Count 2 /Kids [ 288 0 R 293 0 R ] >> endobj 297 0 obj << /Type /Pages /Parent 299 0 R /Count 64 /Kids [ 3 0 R 50 0 R 85 0 R 122 0 R 155 0 R 188 0 R 221 0 R 254 0 R ] >> endobj 298 0 obj << /Type /Pages /Parent 299 0 R /Count 2 /Kids [ 289 0 R ] >> endobj 299 0 obj << /Type /Pages /MediaBox [0 0 612 792] /Count 66 /Kids [ 297 0 R 298 0 R ] >> endobj 300 0 obj << /Type /Catalog /Pages 299 0 R >> endobj 301 0 obj << /Length 302 0 R /Length1 488 /Filter /FlateDecode >> stream x=½KÃPÅÏK5­T*•jZ¿Á¥Z-‚‚"b¿PQÔŦmÒ"M¤J ‚ftœDÄÉ¿ÁQ âê`w·º88´xBƒ7Üwò»É;÷ÝÀ‹K¸ÉéŠ –XyfåÎ*!j–ùÎìÒÌ‚NµãÃ^ ¥švÜ ¬8œ)ªJ¾™Tƒ€Xd-VdÁwÓ#›äñ¢^©Ê ¼‘oÈžR9§ ‚ùŽ,ëJÕĺȓÉ!CÑÕÝÓïsò yÌS¤.À’ànÞM„«ÝKŠãa;uæ^å['ú~`{í ¾±3“6Uc§¦gË%üfSTˆ endstream endobj 302 0 obj 377 endobj 303 0 obj << /Type /FontDescriptor /Ascent 917 /CapHeight 815 /Descent -313 /Flags 4 /FontBBox [36 0 438 800] /FontName /RDYKHS+OpenSymbol /ItalicAngle 0 /StemV 0 /MaxWidth 1168 /XHeight 611 /FontFile2 301 0 R >> endobj 304 0 obj [ 356 ] endobj 305 0 obj << /Length 306 0 R /Filter /FlateDecode >> stream x]ÁjÃ0DïúŠ=¦‡ §gc()’–:ýEA¼kù࿯¤”zÐA³z3³ÒÇþ½gŸHJ°ž` «XÐ “gux%çmú½UÍÎ&*áa[æžÇ@m«ˆôWF–$íÞ\¸á¥hâ ž'Ú}‡ª kŒwÌàDê:r³ÝÙÄ‹™Aº¢ûÞå¹OÛ>S/®[åF™8<*Ùà°Dc!†'¨¶iºötêØý¥4&ÙU$‡Ôõj~ñõŒçÄ‹O=?ÿºcÇ endstream endobj 306 0 obj 207 endobj 17 0 obj << /Type /Font /Subtype /TrueType /BaseFont /RDYKHS+OpenSymbol /FontDescriptor 303 0 R /Widths 304 0 R /FirstChar 33 /LastChar 33 /ToUnicode 305 0 R >> endobj 307 0 obj << /Length 308 0 R /Length1 15820 /Filter /FlateDecode >> stream x½{y|[ŵÿÌ\­Ö.Ù–dÙº’®,Ë’mÙ²œØŽc_¯Y'IÀ1±³‘ÄY$,q laIZ(Ò–°´Pxmd)…@q¡¥…Æ@û …_’–´JHÚ}lý¾se¶¾÷yïóùýñÓõ9g–3gfΜ™93÷šPBˆ‘ŒÈ+/&/P#R^übåŽí¾á¿ýõ|Bè]„èBk†/¾¤0m Ñ¢¾þâ;×¼ï½^Oˆe6!îwÖ®Zõé³IHy5ÊO[‹‡J? ñµˆ×^²ýòö5Ê—ß‚xÇÆÍ+‡¢Õ´!>Îó/º|X÷+Õ刈¸oÓÐ%«îyåiB"ùˆÇ†7oÛN>eÇïà凷®Þ°iÆ2Äo$ÄÑŠ4ЇÿŒDC~ ê#K§R”d öy@¡Â×b_¨¾ù_Ãj…C¬ýxuÈÓ“)½ä<²)û8YF^¡Ké…Ùb2‡l"W“»È}ä)ršÞHÇTêì ©#+ÈVª¥®É>LêÕoèËþ<; mïä}Uue? 2yGE³kaÅR‹g¹Ÿ!oQ­Ú¡Ùê W’CBmœMnBßž WÐC‚9û z3¬$»É)z9c~õêsÙ]ÄŽþ%ÐÒ½äAò3ò,ù ¤uÑÅÂ%“-Ùù°*‰’NÔô r=ù 4÷ žŸS õÓ9ü3z‚þ^Ø$ü ’"gÈÇä?i˜®£W³v:>±'û ¡‡2dÌ!äßhˆÊôB”ý.»Œ]Ív G„·TaÕÙl}öYØs ¼×GЯ_‘×Èë¯.ÚCÿƒ]-¤Õ×g¯@{cd-zñ òr”|DÕTO4Ÿúh-Žž]AÇèïY “XŸ°B8¤¾%»3{+ñÃVÈj”\O®%בÇÉqòòr†¡d %[h/½•î£?gÇ… „eÂ*Yu§êQÕ3ªÏÔ6õ3“¯Lž‚Ö¹œjÒƒg€¬!» ë žgÉï¨@=Ô I3é\HZN×Ð+é~úmúý!=BŸ§ãô]z–þ“¹Ø-ìvŒý;;ÎÆ…!"t÷ /©üªß©>ÕM”L>=y6kÈF³µÙýÙïfßÌžQF¡˜”’ÒëÚ@FÐûýäÛä{Ðùaò2ù ìî¤òœ&ç0ŸR ¬É¨DËhzwí£—Ñ½ôvú }Žþžž¦Ÿ1ÂŒ,€'¦±¹l»†½Ï>òIh.¾#¼*|¢Ú©ŽãyTý˜úœæ´¶T÷Òg&NL’Éu“wNÈÖÁ5°<æ\‚´Áææb”W‘-x¶’ä2èh4þ]XÎ!’"ÇÈ Xuãy“¼…ŸVžw1’ 2IÆSMuxrm¯ÆÈ´ÃZéjŒm^Co¢wá9@¿Oïƒ~_¡¯Ò×èIú6ý}"¬’µ²YèQ/» àYÎV²=ìfvϯذ7ÙØ'‚U° ¢P&t  7 {…¤pXøµðUHÕªš­Ú z^õ z>[=G½\½R}³ú>õêgÔ¿PŸVg5·kî×d4ïhó´Ó´½ÚÅÚ›´?ÒÓ¾¥ÍêÊ`O=h}ùÔ:ÅÉíôBUŒí§Y–A¿ʶ ¿dwÐG¿ÂAÔ{Ñ‚Ud9ËO±ï]¹_øƒðoìBT ×L¬b/‘'ÉKê×TêwÈó¬ˆ|€õðaˆý”ÝÍ\tš0Cuê%¬:;ÑÎØI¦e‡ÀñŒÆr²„ºÉßT瓳Ðÿqõ^è´‹ ²çØ\XòäAì7w“ƒd5ŽÖ­"‘OÈ7éQÁGÀîv“qò>9õe{U±‰6Ö¢q±šFŒÐQº0û<+Ïþ³þ÷ô:ò¦ð lÿ|:ŸÆÈÉÛõßÐU“*y+Ÿ—€Õþ™¤1¡ b}DŽ ²Tu ö›xq²C½]¸–~ÌZ1œNeå^ÀWc¬Áwa­â먙Â\Ç*¢Ìè¿—iûÉkšß‘{È>ò„P@J…°–^PùÈ·È)aj½ ëS1M@Ò%d´ëËþiòAHXOêI=]A—’äÌ&Þì%hù±ÉÙeÙ»Õýê(ùG ÈÓX½\ÐâjýäpÆ<|“̦7“ôä*2†}ÅEKiÖtF½C½_ýˆú°ú§ê—55ärÌÚÅ?±køèJèâ=òØzfOæO+Z1{ØFÖ/ }ªçhu=Ù»²€_?E©ä*ŒÈº¤¾}p¯µ‘§£‹4©.µJ¾½X€tæý¯§ M¥hJ­žÉíä SKÒ¡ÏÃÉh4‰pѶcLÑÆf%^WY±#Ã­>¨ôB·Cý1¨ßïç|sF&+IŽ,ìËÅ}d…'EäX´?ÉyÎØç9KxÎÈç9_”`ɇ•³IARúâÏb-tt®mLÒÂÿ!{u.¿{‘Ô½piŸ¯sïà”Õv/þZ,—Ï ½!o*”t´÷ †4bAÉ…Q.[ú "}ƤªŨWe´:X¥’B}]IëàìîÏóû§æÌÿV(“=ÇK)äËbSÝH6F§škvrÆ×â_kžq¯Ð½Kë^¼tïÞ¼¯åua1Û»·KòuíÜ;”ÉŽ¬|ViïQø3e{‡;± åF4“}âfO²ë–~te-m„Ý2Ò6*ÑŽÊôÆEKûŽâæ»qq_ŠQÖ>ØÖÏõÅÚ÷MµWQ&·I(û?£ÅØe‹qpl;Ìè³mFÐÉ¢V=+<­êYJÜ:úY&„0®¿ÅY\"•ôr¹eIÑÖ¢» ä’º‹fÏ ¯ híDM4VµU£ªŽ]ì¹ÌsYàFé%Ï/¥ñ˜îîÂ_ýÓõ©ûÓ"uLg̰ß.ÑT h’ ¹¡ÄŠ=Äcõ0O¥È—¤Ànéf‰I$Rì÷ŒN> Ö@o`< ŒhÀ)H¡Ò*O†þAvJ„h‚•U‡ù^õûF«óA“jYo$k„EN83“ ÁÒRÜ×PnQ¥ÑØk¢¦«ªf…Ìíe É:àÏÄ™«bJìL Ršì ±¦3M0(nT[¶4ظÁ\0_eý¹+ÖT·ï”}eùE¥îP¸´"?£eE@ÑÂÊ-w…b¤ÈcmŠF)¸£Ñ={ÈÀzs”„³ÿ)Œ Q±¡Øå(h¦QöÓpØò k üuq ¯- Iuþx!7f–: ~*Øjyn"$1Ÿ§c`bîEíP¶ããÓû7v^ ³ò”O›\2ÙÝßpóÞß¼­ŸüƦ†@i©T¿I桎ǯ¸sE³8Y×_( ¥l=»{â'µ×m8pŸÕÙSêaUTïsYÜæÊ+ D¤+¤[Í·I‡¤—¥¬¤#‚•Z™U¦ÃlwánçQó á7Âï„Íj©Àl øü!©Æ¿4 }Æÿ‘Ä~h>bfµ:­¯„¢¯ÄD|U%$´ùJœÉåtRÈ4®ê­¶°o·H—‹Y‘‰WUWËÕ½ÕÃÕ«ÕÕ:‹VÔ2msyyo„F®ŠÍÜ‘›1M=|<{¬lÉ­/| ñpÅ+ð‡õ–¼P¨Ô\j(ÕÅHYØ$Yc4à×—cÄÂ@¡@® ¬-[)Àá/`fʸL f]’¬åéÌ¿²PH-m5{RZ0Ã=ýêÁMzB%•çÑß7̳™Z>|-9xíÆ"ù|õ¼Rãö‰µGvÌ_ù“7Xù…ó-ÎÒÒª*ߢ‰‰³¿NÅä~Ä!€ >JìÙßk?ÀXÔ0œÞ§ÿÏr6ǵÎý#WÆõ‚û]÷»åÚÕV8qB›FÄ—Ç{k7%n­•k{k‡kGj÷׬MÖ꟡Ção“¿“l\½M¿Í½=|þZ÷AòPAWz—» ‰Õ69¾®š­¸‹Ò«ÇÚ2B¨ÞíÖêõyn·«¨Hg€ÏÈU´„›ÕÆlN{‰Íö—ø,Áh)±ŠEÞ±&R]R#«ÊUÄÉ~#í2äù2Ù+äuåý"¢³ê˜NWYÎ//‰Áj`C¥Ë™ïr9õðÖóÂ.7ÂnV.€)ℯ²†‹ÜÜ•wi–`èËáösOߨ×äj|¢ÍJ˜!O§Õ×:E¤5>…+ÑrÖ„#Ž‹µ lÍŽ±ÚVw¼6Ã.Nû÷]âÊЊë¢ÑùF‹Ü=E®‰"÷„k~çêŽ?awjjâÐÔdw6P{ÃÖ›³Ázª¢|ú«o¨rEuŸ2KRBÑ(Qކ"±ž¡Ö±¯âÿùhà«®IwÕÏo°6aÕð½÷ñˆOoJøÂ” DûùÚ?0°÷)[h–<,Å_(T;„qæ;©£¬Šq«D.;Ê QV§ý ”È×4L^P6™œ¼­t²­cšÌæÍŠÕмßÔWÅ[[Ø7;½®ÊüÉZ¿Æ)Kû>½OXÿÙªEuiJKYYI芉MŒíß±«ÍÓú œ;&®fKÛŠËc -ßdO ³¸cõÑ:¹OcïÎÈßœ¿¶`µkg¾¶4ï!Ü5¼h{…½"¼az£àïšòvЀì(Hœ/¬6.v®®7¿gz§@Ñe ©N¯rƒñéÝ€ÚWHhWa††{B­:C½i£A_˜Áfkˆ»[ ew Q¸Ž ~Q ¦‚iƒ9Á©ì²Õ‘¢X %°›htK´çL4 æ 6k¸4MؾOSë‹[”EÇk󖔺œn'ÓÛE/)Ê/ôR¯Íã¥Î ¾âD£‘è ;ÁÐn¡~¬ðXÛ§;skJ]ÂŽM]›å¶ù‚a`"«_Ú9Ô´¢>0/³s|ÃùÜöÊRi”ðÏ =±qQû…öÜóô{´àÝûï»\´×öÀ¶ o­g²KxãSMfâÖú%ya©Õ`i©(½Acåí婎êSåGªÎ?êÈË«Õ×i43|óÕºRWi¹¾\¬g‹·è®‹Ð?TùP»Ažló›Ê]V"4jƒùÍ妘±™k¬*l–í Ír¨,Ñ,{E W¢º™òì´Ý•hÎ*¹ ?Ÿ|~Éôï%1&ȱš„Še#ôRó˜¶3Tb™Í‹ì-œÊyh­o6=Û՘ɎË3S#mŒ»¶jÝ*jiLKá‰jäòŠ6…€,-±6jiÛXÛl¿•'!ÑJ-V[WFPËù¡D5D±µ$ÄKÈþP´‚×'"µB—'*d©4a©Ø\±¯Bè­¯`—õÔ/Qüˆ-°†ÓMg¶D­g¢Ÿã‰-Ÿ5YOŸQ’± (æÑ4å.E쌽!ËMóÃù²èODûÏDQ¿\êQÒŒn—B}hFªDLÀ7@Aî‚p„S68…p¶ (ÅnÓ—Ê÷¢€ÂÚéq%Añ¹ç:=‡8®ksVEŪd‹U sM4×ñޤ!J¡¦PT($)´¢J¡²“£ŠXŠÅbV —q˜Ñ°se4V6V6Ž3—6½1¡PÌ…¢Q «¾ÄŸ(sWÎߣ¬Yܲ`¡pŒàóò~ðl>Äž²5Sp²±õq· öÚdkhP’«¹ “â·úQ§ÝÐâçÍÑç[Ì2sšsšÜ xfp¥üù-ŸWÿ–ïd[a³pw©m?¥rÇ NmÝ´éSþn@£µå+ŽîçieuôÙž‘ξ«ÊÃ3'Cq·Ýõ„çUX3&C3ܶ²fõ¼‰·¶¯ºáàäíê´Á Ö_´šÞ·}†zç¤a•;  5¾Â ‘õ ߬îA “Ôq^+&oÊ…Þ›³Åbë¥b¸vk±ÆíúÁá€)(Úx@rÅâcô4è½-1-qHC52¡ÆbÝ–§ç:)F*Ñ[qˆ”…r£ÑbMÌq9eˆwru4Öq’öI …:œ •c•Õ‰¤“îsRâ´:™ó ÙÛëe¢wÐ{ЛôªbÞï>Ƽ§¼š’ùcXz0tcü¾8œ±‰´ÀmiA?EÙ_;1:¾®ih5ÔºôBY^ºô¥ªöIm³7¿ªM½QIå 'gLxVNWƒ,à\É–Bos±Ç?Ž=ÄDüt±ìz¾ˆ–©ý9d¢Dë iõ:C‰¬âº0cùTÉ¡h¢¢ª"‰÷¾»N!³r¤E!醙 ž*ÃÑĘ4.1"ÉҠăjYºGC‹]´3»/Óä; L£ yŠ‹ŠÝÅ‚Æb²—¡—%^Z¨·{‰K[RFmFsõ f/uä9½¤Xí,Ã&>u‹D#óÚûäš0mÀËÑ9ÖFõ°f·q·uØ=¢ÙgÜgq¿Àžóvk‡MÖݮ}ÚÓˆeŸKÇ~[ú±–㈯ö¤Ãîïä'L¬ÍÓ1Ap, ÑÉ]¯^²z×ë¯~÷xí§Ù0»ªÒ[fÊ• Ï^ýÎÞ篿Ÿ†Ÿ}‘Fgõ¼ý‹ ³æº3—Sÿ#»K `úXcfO³_c ãìy™¡ºÀÚ¢²šÊó­%å*M~aþs¥Ï…~k}ÏúO«¶ÜZ©·N‹Ü`ø¶ôíà HÃaÉ 6ªMºòã,C·Q#d#³ÇEr€‰”òõ€ò}ö^JqYÓ‰[¡ö±¿G]¢û€G,*⃖ýE´(C7È^÷¿ÛíêPTk÷†ì{Î1“í z¡«Ë©Ãú|Íóôùl ñ[ýLYu K" ðÕFn„͉fj.²$h,± ±<±9±;q(¡IØu>.„c¶Ä¢q'È…EåaÞ(”¶„)â:Áœ»k¹9qk‚Ÿwó 6„uð1Ë›Ž³9QD'çû[tMPa)¢è7 ìÚÜü>Þ »û¢¨ß—[1OÉzÈð_„ò¼'iˆP(¤(‚8M}!+Ú:º•Ën*‡]Pr± Èê2;L…¹Jûálð6z½^K‹7“ýCÚ˜Ÿ£ààñØ•Æ)|G‰:û˜l¯Ú Fµ\êüÏY¬ïóe…ZqE¬ïó¾Ë–˜œgk‰Éz¸Q|´y79SŽ‹×\Z‰¦afާs]Å–PZ‰Í©¯ÉzJ+±_”f²M;ENO?îòµ‹Ýþe>ñöEû ¼hîpÇ!á.„{-ÎBÕ玴’P˧¿)ãx79'šÝa ̼¶µ¼1ßGCóo;¿}Økðú­ÊïuUÏlZ{weÛ·o7Ëc³º„ŸMþì¶µÓƒwùó7Ÿ?ÿÎÞˆ!N{¿ñ‘ê®YëëÏ[¹ñP©ÅGš’PöïìNÕÞØÝ%›÷ö™‚ FâÎÐ#U~¾Pp-£Ÿ¡Ú ÃVýj³áŽÑ,—¨ GŒEªR‹ZT3uÄQX°3?ß!CûnRÖ’@"æsŒ;‡»h>üL~'ˆCFîNù5öçùÖ{pEˆízâô¿KUNM8x`‡Å›wj«-r78KàæбI¸1œN3'NXBÖÖFïÂ#ýWØòv]=Ú¦š˜|dåÄÓ c%+ ÇVÎ ÜIÿ)õÿ|'ïkKö´ªFxˆè·Ž’ Z÷ÃîºÞàxécÄ8Ǩj0ÞSü£âL±ê¬ö ÈSÂÏ‘EM¢ÚêPÔÒ¬–Ò ¨–$KPtH’—»e’Z£Îs¯Öòp€4DÉÍüˆWÃý/ ü/ ü/ ÷¿4ÜÿÒpÿKÃ]/ wÂ4ÜÿÒ(þ—†Z4Ô§9®a¸Ô0 ÷Àò‚Ü• Âó ò! ……ÂÓâ4ÉeC²’ ‘œÊn8`cA*“A Y0_,  _hÒ¬P8` …Æ)„q";J‚‰sf3™ÇÍ‚Ù-MybŸoØŠ'öùÎÄmý_|1¸bg¾ðÅøŽ¥ÜJ lÉmêÊaq`+\¨œëÄþÜý®¦F}Út¾YÔ /Áeº¶ýúE ®ˆ”5Ó«åž`I¸¾¬Yxh"È}¥«zç ]s?ÝÆ=£‰=«½Ž¢ôÃ)?‰ÎŒg0þ1ºò1Å›1òUø'¸²Tá‹›¹d®ivQÑRO_Õú¢õžµU7y2ž<æ°#œ×éE]¤Ët±æbíÅÆ»b“‡‹^w›`U¦˜É3kŒZQSà. ¬ü‹•ˆeÇ!æG ÊÂÁ¨9ë*rç¹&“ k’é"Bó‰ÉL(õÇŠÜx³K´e1äAªVß‹î÷Z‚ïyq²ÔhÔš"b¬9Us®F¨á#bÊ'jp7d)ˆásW›Nuy¹¯,QÖ_úE”¨Ç1ÝÕ5ÈKû†i‡«!~?p“N¹ŠnU,î ÷Xqà?cÃPîP†ë¡†tUQåz×Ä.%»²7|í"èkW@ZµI‡— Xã¢d@ýÕÜàð5M¹àÑRö•[~8Ç9‡€þmòÕŽÖ*úךpüà%3jšiCUcÇäG«k:×.ºxV">“RÎâò„§…Øcß›mÆ]NÀžüõ|gFi+-UÏèžü¬iñòöÆyr{È`(‰Ü™óð:€í›óÖ\niúHçæõò\Íäs_PXn'7bà÷Á¹¨Ö?ÙI.°’Ooúä$Þ-}ž3Å@lšZÌc x¿CÈRö¾—{ž„Õ¨Å.ÜJ¨éQm#]à‰ ²lt±/£2¦æ8§)‡3žQÒaŸhiµªìdÀˆ¸° (˜YeO]^+g@¶æÈ¦YŸ#‹kå'Á>—ÔfÇTö´Óç¼é"» ì‡R•5¼"áP:Ï·‚ÿ,ñF9L•¸Œç?›vrñNYlJ¹“©êD.¶ºâ½­ùÂ[hϋ«8“Šø\ìU˜(<ZúœðŽi¼¦-Öøê{ì;I9² ì"qЇ…«q«ÏÙ~›2çêùm*‰·æ  W*,Û„-ø4I6 RqÑwLx-•…÷Ózoßû)kAü)á]aÉ×ip9EËSÂ&ðždÒzS|«QÈ ›¨ED)¹WÁ²ðj ‚Pß„|$ Ç…=¸D…G„kRâØ1áJ}s)¨ï~X 'i“9>ÖªîGnRø4þ7¥¶Ó¡ú8i ·jƒRßFèm„¬Â}€aúC󆿴â-Î ç xb 2,¼IöîEX…ìLAƒ|èv¦‚áøQá*áJhÂz º£H½:­7ó–]™²;¶+ùoyJx,0(ë >#7nSº²?íòð¿NéPݹ±€¤]| žF„kMìQ4ü)¢”X„k•ÂÙ´ÑßÑ_Œèfà}€qÀYÜJà`9@@ͽi³%n9&,U ÏI™kŧ„ÙèúlE[³S¥Í³¦*KÊãÿsÅ‚Á„ÄUf•&ºa? „ù©U"Ú¾0¹\'óÓõñêc¾æ±”(å’S·èJésv՞γñ–t(ŒÑ”άäG§¦¤Iç;ãb«UhTz[ L„é¾éšé˜'µÊ`ÄÓV;¬•Wz'ƒ$*ŒqìqŒqŸüñ‹0 ÝF²c;œ`©jH `àiÀ)€ZIDˆ!½5 ï0HŒ!n–ƒ€ÀAÀà@KŽ •¨§ÜÕÀ#€$à$@…±ª@;*g|d[Hv³»åFº›ì¦»ñ!ônÕnõnën›N®+­ˆËë9ªâ¯—ãÓõÃú½P­—õ½zÁª÷é÷9µµÜñkk×ó^Ï'=‚}ú~Í~-;Þj¤6rp ãø`ï$à,µÊ7Ç›O6ŸmŽ÷œì9Û#?qòÄÙÂñÊ“•g+¹ÇÓŸ¾œn¦»ñA´JÄWÍ-tU-6 »…}‚JbB lA5h6Œ~Îé5VƒÏÀö’†1øAÔŒiÆ5§4ç4ê^Í fX3¢Ù¯9¨ÑˆÚ˜¶E+kTçZÛÙ›PêAà$€‘àýJÈ LÉð¸ç©àa%.÷*! ¸š‡dý|#Àû˜|J\®æqŽqì·àÞ`ì·rq :(™5è 2|Úx.Hǃ§‚, ²±ÖFöø'¼•o $IÀÕ<ÐÚ×¾×ÁÇ'þð~%tø_Ó‘6¬äÊÀ½JH®æ!özJšniu²¸ø^ÀI€@bÀ-€ÍJL¦ì°ÌîI—U`Ãg÷¤BX#A9âÍ‘b…¤ÝEñå­vDÞ‘÷@$‰€Ëޱ»Sœ÷îÔÌi¬=Ù:»(oÊÝä/ü€ïUB1à%Äs°T}O"tJÉ>¨„x9.ûðçevž»‘ba»ºK60Rˆ·ÄnÓÙ3ì‰Ô:»˜a‡Sa+H:GRœ´:˜Ý›”ËYý‰¾WÁw(ø[dƒdú§dúwÉôdjÍÇÏA:§àw¼^6MïMÏMM÷MÇèÛxÅ`¢~¹(`úcÀô¦Ç¦G¦Û¦eÓ€i^€‹ 1±ŽéE .–>Óg>Óï}¦_úL/øL÷ùLý>S£ìôo$Æï*ø; ®{Õ‘V²—‘:Eò¬TWIÒj¢SéÀçò¼â6:ªH‘S‘j°5§"!™9Í5¥ÖD«O…¡j:=þ>47mª‚r>>OÒ šÁI©È£`SkÊA¼©5 ^mvLÕj'8eƒÙ–Šp.k*âJ dÒä<¢÷' ÷Óæ =?%~"gt4%þ# rD|¿g…ø—ž <^ñ=ÌäGˆ'Áz¢AÙ ¾9-¾¹& þ"Ù#¾©Ÿ í3ácbºÇ+Ž¢aÉ5+ÄCk ? ¡XJ|8œa¸Þ®™'Þ‰Šß aŽˆßó ¼º.²S¼&´G¼†¸½ç&q[¤D_$®óŠœâºÈyâZtäb”Y½æbq(r»8X§´ø¢ÈËâ"L‰Ýk”ÍiV2f¯9OìB ÑÂ3Ђ°Ë8ŠVÕã:‚§Òž~Y\2ýI†]˜Ž¶ÊUÚ§´WãH¸XÛ†ý¦L[ªõk½Ú|_˜uF]žN§ÓèT¸ Æÿ8±|~³åGÂ|rjÕà@ Ž!ÀVýøOãHÞ„¤Cèf݋ڒӣÝmö¼d}´;©ë½°o”ÒÛúiwrl%é^áK~¼HÊм…K“j©&íݤ{q› ÌIvc†’Å}šå%®óðϥ₣âº[=œv]wk?)ÜÑâj±7Ûº:þ4¨$vtvðk¤©ÿ<íËŸ+Z’¼³{Q_ò‘’þdœ²%ýÝÉrþIõQ¶‘­ïì8Ê6pÒßw”®e;ÏãétmG?Øf(l¤™méálliælH_ö6:ŠäŽÑf δ€Žr&Lš ÓREmÿ*“p3mW˜Ú…›¦ïç*Œ ¨Pæ²pËQ*Œ¨7*l.Î6 ¡º5@ý}£ñFCq%{á—Ùá\ösÙ?æÙJ¿Ì¯Sòb çG±¤…Áó¥þþÿ‡V·ý?ÔIÓ3wlêëħðƒRçjÀ`òæk]É‘>ßè¦<×Bƒ+V®åthur‡´º#¹IêðÎTÊýKvÏž)uŒ’¾ÎÅ}£}òêŽÔLyf§4ÔÑŸž¿§~Ë×êºé‹ºê÷ü7uíáÂêy]ó•rÿRמ=Ÿ×µ…×µ…×5_ž¯ÔÕ}^íîíÕ‘¶~¼_Ph_{a¶ züým…ÖáfeêÌð»®ö<¡"ôabÀêFüOƒ ÀgUeke+Ï”æYfþïSY®«gø=OЇ§²¬H¶Imd»«s]þ¶á·}û¥øaL¶mË ÏãéÑN% ÛÆœs@—ùÛ —1õ‹Fs¼d[´½o´§ÿÑáŸæ~w´^‚€S©‹ NôZqô Gß )¬ýMÏ{>êÆÞý)ÅÃw?8ß+Œ57ŸjÆzÆ{N÷Äø‰S'„±ÊñÊS•Âô©ðªú)šúåsitÛ¥<9J•Þ*ýF )Û£Û à)5 †Œí®%žÇƒ¼hâ”Ìh®HÉ”’Û¶# (©J/ÃK]ÊÅóìÿò›JÅü˜– endstream endobj 308 0 obj 11467 endobj 309 0 obj << /Type /FontDescriptor /Ascent 891 /CapHeight 792 /Descent -216 /Flags 32 /FontBBox [8 -216 991 678] /FontName /BRVQTP+TimesNewRomanPS-BoldMT /ItalicAngle 0 /StemV 0 /Leading 42 /MaxWidth 2000 /XHeight 594 /FontFile2 307 0 R >> endobj 310 0 obj [ 250 0 0 0 0 0 0 278 0 0 0 0 0 0 250 0 0 0 0 0 0 0 0 0 0 0 0 333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 722 0 0 722 0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 444 0 500 556 278 0 0 0 0 0 500 0 0 0 389 333 556 0 0 0 500 ] endobj 15 0 obj << /Type /Font /Subtype /TrueType /BaseFont /BRVQTP+TimesNewRomanPS-BoldMT /FontDescriptor 309 0 R /Widths 310 0 R /FirstChar 32 /LastChar 121 /Encoding /MacRomanEncoding >> endobj 311 0 obj << /Length 312 0 R /Length1 28160 /Filter /FlateDecode >> stream xÕ½yœÅÝ?^ÕÝsÏìÜ×Î}Ÿ»sì}³°,Ç"¬Èá*È!Šxq(ƒDˆ"â*â"Ä(áJ‰G\0>‘Qc¢Q“G²Ùø¢D—Ùß»jf9Lžçõúþùc¨îêªêê®O}îúT/¡„-¹™ˆ¤0é¼kþž{`1J^#„šæ¯\îÿjÝ5»‘ÿáîE×\¶tžýšI„Hנͦˮ\µ¨ûð9„Èp9û—‹Î[ð?s:ÆrÉ/QP¿ò¿&d..IxñÒå7ìï«ý×aôyå•WÏŸ÷ÄœŸ 2ïnÔÿlé¼®Q^§ý;!—¢â¿jÞÒ…¸×݉köNþk®^¶\ùˆz6®oÃõo¯¹ná57Üx´’ùs Q?‚2Šû§EVÎsÿç D”dD®PªÔ­®Bo0šÌ+±»ÃYér{¼>  G¢±x"™JWUg²¹|M-©«ohl"ͤ¥uÄÈQ…¶ÑcÚÇvŒ?abç¤ÉSΛzþ´é]ÿç£ÿß*/,5ŸIfaôÿÿ¶’Cd6ù%9F¶’D-¸„ñd5¹‰ìÇõÃäirDPÓûÉ[týÙL7З躷>†'YÄ 1-}IR ý¸ã”m ·’côOÒ»ä÷d"ÙD~/n#«ÄQ¨YEž¡³ÅÑÄC®•,üz;Ú¼Œi[ÈýTMÓwéïéF²“¾Bñtq&ùýmâ-7HNò…X# xÒýxÆS¼ô‹ò­¢@Ÿ ïÓrØé"ú Õ’§„­xæõôk²í7Ð*r¹‡ŽÂ]*=޲µd&ÿ}ާl%›è¯0îMH/‰“ÐþŒöuá=Ž‘ýôZ²@TÒµdé×b…hg}‘G1Ö[Éf²U¸…vÐ{þâ7áH¤/¥'J?\ø·ò‚^ªž]üÒyhͨðÇáÞw¶+\F_J·¼á ½_Ï>é |ÅžPTšü Q¼çÁ28ø+›5è–´rú°Çíö zÝî=ÂÊ”j©¸€±ÚBN¹V^\àw¹ýƒ>·ÛSl9} ’W†äâIY ¿¯0wD¡µ~\MS.Uœ<%– f›IëÑ+h—¤Ò“¸žê¦¤§GOlÔ&‘.måÄX}딜?YªðR{9Ë06U›åV‹ÍG½“)lÒ áxÀ«MLŠUíñêªHs¥Bf}hÚÅ˦mzvá û“Äh –GÆô^SÐæÝrÉrå„…7Nšøâ½^nÿ´*Mšþ¨¸ì¶ºêáê)UdÄÌŽ‘çåZBÍ_û¨vR" i§ª“1Rµ:iPÆ«gŒkšî(d\vËN¨(džŠbQúœhˆ¤HC®.¤€&%RmÁLÄáÒ¿àz©íEë -ª"/e^_¨;Ôò|{*×^9ѦMÚ‘rmZíUæGªµÚ¸²±DÐ2èë7`8P)0êî?Á ¿ü¸‡ý»Ã'ÙœâRg”^«u8mñ®¨Ï ¯sÒè ‚QTé_qíój‰ùn9•­^ôßÝ—çhÂÀ!ä0 H c¼3*ÔÕš8ÅÜc{S6ç¼ÐlðMÊ/¹âÂYÍ—F fi«ucâ±âÉ;oùç£÷YmζñÒ©=»hÍÝ—Ìåï°íô;Ô‚*ÑõŠ~Ÿ×©rŠN³ÓžP%Ä„9aW>ÆßȬ%x©ÖýÝŒë ö±·O«7B@Œ¯åmv›ÉjJ$E·µ›Æ4çò•3,†@'ÞjÖ¬‘ÝQƒ¥ø¨eCâqªÚtóWß½Ïfq¶ÛZ|¦gWñµ»/½Š-ã)oЇů!—;ÈÊ”±#ÛZ[êj««ÒшÏë±Û ú ¥‚«¬l”ÚöÝÖJ¼²±¬ºJ6‚5•r ÞN6–µ”Ì«Ù26gMXFŒöö½××Ç8ã¥Si*_Ë÷æKŒ=K#²e‚VÈJp×—p5CK#<Í"g3»Œ‰µ*S¥¢BRjNQ4苟V(tšÑÚ ™¼B¾e³B¯ ê‚/y¸øƒQAÁk•J­Œ*]&×·Qœ}­ é-“´zQ©GýN®WÈôªŒJ¡UŸ<©Ñ(•+‘ÿn”¨S VÙ$‹^–QŸÖ¨Åÿ,/%‹¤+¤[‰ ½J«ð9Ý¿²“×´¿2(^õÛpgɦ$v‹Í¡÷cŠû[1»Œ°~'κÌÒL¡€‘ã`qXŽKÔ*ì¡K‚UéPñ¾xÐ[U| T•õÓË“`\x)í÷Ç‹Âi”\÷{«éÊp:,->/VÑIÄAª •“Q#U¨‰³B<ædÁD©pZ§ î5ÆÆѤ ÈLxO ¦¡•6ÐÏ••¾ßëM’Tü›ÂQ!*”ƒ@íA³V%3(NõêM‚X©0i5*pm]*Õè1Ê$QR¼A~cÐ+DÒˆŽ'¡›ƒÑLY< ÉЋÝnGØ+½fºÔi»Âl³™¯0yÓ'†¾Stx¯¥ «ÞÔÊ]D‹a0¨ög#g18úÄÒ g]¹tæÌ+·OYpéÔ©—^Š÷údè.I’m%fâ-DeYdÌj‘ rKé­@ˆÇ{³²Ó FIBqˆ¬·Å\î¤\&\㌺¼I™Ì•'\Y˜#ïÑ’ÓóÚ·-šE-­•uj:í´]6Ç8G×e^b\¢[`~Hób Ý"Û¥Ù¥ØIwÊz4=Šƒô ì(=*{ÇøŽî·æßÚ?1~¢ûÔü©=¬RXE…ÞíÀ1r¼pß D8&¸™©&î!ˆAÁh`y£A-»ùæeËo¾yùá÷ß?|øÖ??ù¯âß©ñ_'©á›¹t>­£µt~ñáâ1üâ8N%BįeJ m…pÀ ÉDÝó>c¯Cíw˜¬âTù$¿Üjðøåz7u Ç»{B3Ör$ÓŸÄTgÏÈ£°‡µ0®´µP#½^§“ùbA?Mh¬³íñYÙxüÔîx<;k§”„ÇVMÅ÷›£žxÿâñ Æ×žÀ{þ ð®!ë óÃrØ*ºž5jñ$"½žç‰×¹Z{£lµòûÁ ÒcʇdÛ¤m®û}8·ë·›vËw+v+wËvK?vîˆô(DžS<'ÎuX:,sgÒ5Ù($SX¦ F~Q­Hû#v±èñâñÞ~6L ”é«™þÁ^ÃÑn.·›²œ~GÑú. ­À¯¤¤–$‹ÕBåz–ÕVÆ÷¨º®æE§Î¾qÞ¨e!¹.RöV˜ ?Ÿ¿óƒâÓV¯¦¯J±@ !êu¤ªÚö¹Ýµ´ã¾%ëkÓJó˜ôÈpÀ/r>&ó cý® ÖªzW¾#öXƒ=®ç­oE¤%Ôeë–(—H „¶ÕÊÕÒra¹m]å:Ë:ÃÎA®ðMįU˜wÄpv½¡o`x€^Iï?Gç óŽãò„‰qÞõ½EsW¯Y0o­~Ý”Gßû‡GþF/¦¾y#WNÉÓ±ÚÉ&i†t1l°B!­´8,‚ÇbÖ¨)Üb j_ñž7E¦a‚þ óéšJ)¢gæYÞÞÄM!XDкU§%CŒ‹ƒa"‡†ÞOGC¾Š oðÏ’ü¢ÙJIt¹¢¾P4-¬*>cMTzÃj=]FÅd>—N&èq%˜GY ‹èzi‘¸˜èàkK¯Y?P¼Fþà¶YK¦Ò$Eœ¸Ú0Ÿ%[¦°?kæ œŸ{µS\ë ¼ƒk|€ï¬¼àŒ…ÂQ0Ê(=/Š\$Ž0\¢d4`ÕuVj‡ZðX˰²SÅûö÷¼ar :ô 5§kô‘J‰Y°•Ï…Õ0þÛ)lF.@è´æ!^{*R yõ·è’”³gª%À¬Öá E«…Ut `å jtÅÍ‚Ïçà+nìaoeÂÊaõ5pþÏðKj Et¹”>©•U{a¿ÇV-TÙ>ô„÷©äQlŽÇ’1¦–Ç¢þ¼á½¾÷ ö¹K4Ñ7±_<^Í\J˜á³v !ÄsQš¸aP²G@ÕŒ‰Ó«NïZtÙôi‹®ŠT¹êŽ|•:QTŽ Ä~ûÐߨýË$¤Ü†Ñ]Ónj麀þ¼~¤½Â9çÆ‹–ElrûìlüÒ†I¿Ø¶z×Ä©D4Èæ@ ïÝ/“ÍÄÈF’…ñ5yØJÜõ™hk‚çGRý3:u¨õ£LBý‘91JRI••E–r´8š“Ž‰É9ŽÙIMÔ@üy’2¸›’iEÒ–e81ˆ¡÷ö癫Œ+=ïõŽôz oö®•¬.>æ’Â^–ßœ³C·Yì¬äI+ùÌÆa÷$œ¸¨ë»›žüAÕ Fsåm ßýËo{¹×¦Ïû!Gbbtþ-û·Éír¥AÑ’ £yq»cã–⯋)¾\|)ê±åÆÐ›ée´›~÷σÅÅn¯.Pc­4Zuß…bDé•ÝÓ˜á+ ±Vñ|É’-…|§0Gâbšôøþ¤îË<¢¾7nËŠš =<>lÕK›®¬2˜©¹:R¥H§sÌSp¢»sã(œ/±I8ÕÛœà­íèW„׎UvâEÒãWÁkDL;ñ#X»¬Bfèïï7ô+ØIÙ?‹tÓìCNšLÔÛËžà´%®6ŧݿ底>øÎ]÷ŽÈ¥æROCÌ@o5ÖÇ£µÅÏ'¥2mÝóŠÒŒy£³© Å’ÉV³@S –ðybƒ/ξDÔ70à‹²\ÌËðI$]€Ñ2ÀH OW¾–óªO» =Á>'é5I )bIXÀf%.—]ž¸\\%®’­J|?u»Xá:Í’¯2Ÿ6%¢5U„Lr¢ÓE}¶¨¤KÛTD_™©1 §Ž÷sDâžà#&àXf _dÿAX%Í'Eë†3æaËÚQ‰‘•$%·AîùÃám›{>}ãÎ ßY~òÕb!JO&§ÃAúæñ?´¹òòÙÓr7\µé’«®¾xõì‹g]òÍ·ÂÖû¡ÈŽGÎ[Ýqåœû³n¸ÁÀÛ¦ ýIš&½„™º¡0S™VV sŒKŒ«÷»2>™ÝéÚ—}Îz(|²êdZ·Ü½ß-³J+:øâÚÏ̽â§ÕûârŒ•EíQërÇrËÆÄöªýU*ƒMNrA•M—Êæ˜ ÜÛ_2ÃÀ^˜öt”Ùa&¸I»¯–¶:Æ<ÊNå’‘Ì슒vÌýNz.Å'Ôá`Ä+ù£- É|÷¼Ÿ¼ýîÞæš][|©L¡k÷Å_Ÿ§_Þ ­uÙ—îÒd»õ—OýîwÅS@Ř„×Ûo¨A¨Ž.ð3séР´8¡%i2«Ð¬²k{¼boÒJz½}:—Ò%¥”)©EÙ"ý(¸+öœò9I­÷:¬’Í r]¸ÆeªõëˆÞ–¯b“ŸïËŸè?3çýýypØbë‘lÄ[ö®3j,ÓlëÒ„·ˆyn”^ïåýòÏŸÊä¬VE"©V“¡µ*K=úþ¿hü‚ó;ßÙe¹d“(¾÷·w‘MtÌ+®÷Ç#¡â Å¿¯ï›1eœÄåVd1®äŽÂÔxu&û¬UMS}³‚„|Ÿ¤H†~ž¡™~ªÇùWC_NÑ[ÿ2Âä¶Ôð‘)¥j.–wç¬$F?ÑXÖ«o²Öé$ef¤a°CÄе|E«@%ÏXl£"KÝ×öÙâ”6ëFvlÊÖ”G|zèÌÉÆKÌjÂú[‰‹þ/%‡2áHjr¡ÐY‰V‰ÍÁ£ôSýTî ‡\îpØUüZ°0ʆñ?JÄúkãéÉwŸ­«OVM<ԙ̎ Ç>19h G«‹À.¬á§è¬ËÔçãÑ”ÛaL D?Ézúy=­ï{üuôU^…‚Ä"ÎÛaªPJ Ü1u±\u(ç–H5ý¼šV7Dôn«ÆÐdx¯7ßË¡É@1h”Å`Á`u.¨†Á,ÅaÞ(œÍJ¾Èÿ *áâX2ÍÁp,5jR*WC¡`Ò{ê#ªtC)pÃîâÉMp( Eü"g‰X±žÂ`T9!MóÑüOq,`Ó>ô‘´Yz‘÷OÌÎÉα-±-ÉÞ”]m»+ûXö±Ì“þçl‡j÷×ïóëùx,69ŒÄب§=£”Tù÷Æ^Gê³|oøSï>Ç[­­>Z­_^³¼a»G¡W´J¡*‘Éb9Y‚赆:+ÛÂyÇ`ו¤¸#qb7ÀÈ1Š–ew¤î®™¢FfeÀsã/s;g'%Œ:—шÛOz=•º§ÒHê´¦¦ß]SË!åQ3ž—¢Laý²~ƒ¢¢P^ÑoõÏÊæH÷ÿÅ-ôp`Ÿ¥w22 °]ÍÃfk‹le+¦ Û†·wÃLÉø …IÉPÎhI2ÿÖ–K£4 gZ+Š7]øÄL#Q?×ïÑ«ÅF}wÜUÀõ¸ÿ KõÇë—ÓÐ ŠŠ·›_ðÆB‘ gÅÏ'¬X¡^Ìñª ð¿ð“©…¤R#'f1bíq÷‰1s¯&â‹Ý6³N²PŒ’œ¼Ùiˆê- Ã{ý½Ð5 ¥Õþ3Â7j&cF¾`‰Qz(ÿƒ"âÞ$Lààà†L8œštÓM™¾A_‰TMzJ°ÐÍiŠýâ ,Oz ch#Û WËÚôX|³¶eH¤­ÎSÁºJ[m¡Ýƒu•¶öÂBò¬çÙÈäÏ ‘½ˆÂ‹„½ž6ªŒÔkzFH}µQ—¨ë­²„½¤Ê<GÁí©P¶j[Üzã(]²T4·Ö·èFŒŒês£ê|£á—ëÏ÷èë3|løx€¡t”l˜Ì5•ˆ 'eKjxú‡%,Õ”–Abšo›%å v£]XüNE®z»?h0¸ímо‹Ÿ4'óãRÖ£út:™®xÕT O!KÛlCC2èß^ÓÑ~Ằ?ˆz¯óÄ!uãPÙ.:õR] £·3ÌွÓ „üðÚRú\Ú˜¶@GoŒê‹—·HñæÚ@‹_æV惦ž¤»Ï@z”Íþ–€mŒ.6:ËÔÖÈF·Äjtµn£ª­Õ0ØÚÚ×Ëü–E—&b/”FŽ JYâ œÒè:óã€ÉfèŸÅk\ß’5Y¾Þk§§ÙÓb£Ä¶(Q3±¾:5ákàS¢Åììj©iŠ[.®Ž8éÉØ¸ú\rÌß³áPªIëÊåì®Sýe9Áø.Ù€`M nm!.<âþA£Ç¦©ï½d"ô‘[ 0߈BZÛ+šzUûÄް9lOSº]­¹ÖÞbdÞÎuêu‚V¯#íV<ËåNœ½(g ¡`IG ”/ÍA9Ç2tKñ7ûkñuZ=ð7š=õ§§^zé©]/¼$L+@炟XhwññSZJÿøGJ‡úþÈݳx·™ ¦G*AÍFЂå,ð>›ªW FÃssÄ%Š%âjÅjQésÚ ’ݬW)…¨_®%2]H#úÐX·Ù‡Š Ý‘9hÊBnØŠ|»ÞK¶ ÀœæsTÅaµºìMgÌïùçyø‘‡Á¾ÀÚìA¨}k;fB/¤¯ÿcðë>+™ŠÙå+–-\3ö –4ÂÃ?ýé¡âë Þů9¼Y<ÍÝ…«ô$­øzmŸ?qšó½òt¯Y{@¾¯1\nHU¥rµ5µ -U-¹&e‡eŒµÃÓáa™ná™á]h]äYä]a]éYé]§¹ƒ®÷n£[B÷{·Ë~¤ØEÖöРÞ@ĺŒÎ­j¯ÓÅÅl#‹ÂÐ/SûO¼Æ–¦z¾,2¶Ç™¬C6‹¡$x8Wõ™wÏÔrÎÿUyŠ™‘I_}æ ÚØ÷1m(þ÷?.Ý’ÖG§Šø.Ô;Ã7Žn|£®s—ÚV5·ºgû‘ÞÛ{{…<û{ª¤—·¿*öŸ.wØ3‘”Ç7fôøl¥kNz#ÿ𕊃|P„+FÝi/Ç…ùaáB6ñµ²©_T½²ZQÕJã­Õ£T)àÈ(Ã(aT%é ôeìªÞzC}¶&î²’ÓRÝšR £š2Ê85¨ƒ£$©‘éâºF¸â^‹3ÓÆD-, (Æ€‚;©º0JçBw t)Ù^œØ›@æÒŠc5~E…¡®Fî0‹‡‘J,ﬗœm€”BCXPØ ­Z¬†”¥·¾~ǦÛ7¼ÕÞœM6÷Ãñê@s34•Ýùàµc&oúneÂüj ’óÚ\F.¾Ø2yÔzÉU¼rá‚…‹¯;-AïtÇ‚AO¨jËËvøõqwñ5_4/—hhVmI»kèM©ñDcÈË…»ÍãZ68¿ß²¹åAçÃùGa—ìþhÌΦž1Z9÷Éx°*Bäj±Åál–Ú|UŸÕj>3Á—ÑVÛùÔ·¯í@»mÔ¤üEù…†Þu š–˜—Ø—{—×-oZm_m^aXoØh^׺λ®Î²$·:·1'ꉻÙál æä ñ„UîV$¬#:îv/|«%ÙÌoV6bú‡yqIf3QÍŒÔ)Åý{0¦£dÅøJñPe3æ¬ < 9- Ç’ Q—A¥Vzàòµ?˜ɖן?õó†' î`:(UÄÃužŠŠÀwÎ[õÀµËfÏ×oŒ¿3TÊ5|jÊÆg¤l“n¼ä¼¹Ïã÷<øÏc«Ì½¡Å&&­ùúæMši°ØxçS¿bÚ’Ý|-p<‡8»QnÉAçuYM*9‘»bÖÝ£ë«òöFcU™¤?ê U˜Œ¢ËªS«°öêÊYLJÆy ÐŒ÷è“ã2yf}¦ëJe~ǵfý•gG‡£• 3‡W]¶¾ŽÅ‘2"Ö›êJÒG Ë‚kX¼ ÆÀ炯*ÔB8*.Œ†©FˆFã^A]üh·™O½ÎmæmÅÄŠÎD$ì3ЉzŃ&? ‡Tb,rØh.áଫnÂøóä¦Âävk»³ËÚå\`]à\nYn½Î©ª4¦ó±÷f’.˜z=AE_æ@M2Ž…B© ÛWÉÌ_f#ø+c±Ú”¶Ñ OIµJÒOâi¹= vfþæQcæsÑB80ûaXI9#†‡-…³ë8ˆê£³ç3ÓRVGè¦IwÝR7¥.9úýl8’n®x÷wGþ*!®6b&Û©÷®^˜Ñþô‹Bƒ/Áí#ÿ©}ÿøÇ?~Íp bè„ô<` 2]âp5+0`‡¢/| á×d¬äšÑ0Ä•nÉ:Á€˜Í)9?ÞÍÕµîÓC‚q‚`&‚À¥z6«&¼DÏKQÎ}ÌÐV¸{ùt´qÙ¦U±kæL*9SÁœØ²{0ƒh–|â/Ó’Hg¯ÂkçúLU²™Â™”¬Õ·úÂî¦Ñ „< ª3 n‡¸jƒuZgä ¶ë„‚ÃéÆÝL6ò/é¸Ê +…`Æ›RQÝ“7öIQ¿>S!8+“)ªl&Aì@ÉX+&Zë'fZØÐ‡Gži=Ê-u>j Qa|*è•Dœ?„ ò1S¬à7#—Â/ÉFoPÂÁ‹¨ä`ï/+¹v×·‡˜jfAÃ-gÄoÃgÁÓ*»·ÊãT½|é#Õ©D#c F£Ú;E£yœÝ¡z÷E…ÍÙiA˜¥6óÛi{C"Uµ €˜á7š\§vŠ.(êy‚A‡Ïbr {NMqÌAñã`ÀõFáap›¸a¯©%k )Y຀Àgì = {Ÿö~À¯¶D§TOÇÕ¸§X&×èÕãôuœ?@‰=ñ1#H7®ÆÚá â`3q-¶Kn=æA”5¼LÏ& ¬†ƒŒ-I(Ë î·t§2ÌZ†éhˆ§ ¨Q ïA[1Ò–ÌL¼§¸ÏØ Ã¨êÏ-‰\[à‹oõÉð„ÊG'¥R­†âÓÜ™úñÐIBÃ$nN¡{ýñ¤—ÓUOñ&±8јdÈU…ìøÌ8ûìÌ ûY}5àï³÷xbŽ\8ìô“œV«–œ9Æ,åÔf{ Ž…±æ`xñmæ\7 ø °Ì£Å£%ýsÅD=Ž6üì%(°™nÙ ‹˜0‚aN™V4ÐP[Çu¨¬ÜÊ‚ñKz@¥7©)Ä"ŽBkK«Ùtj/}]+ 4;uGªÞáFJ¦ßèìÚp V£–p°*e˹wþ…ŠËUNmñˆ/w4GÄ66~ÄoBlðÐè› ~­M¯!ÿ£øÂö?ú>‡^©±‹4Ãåì9OƒU|* ¾ÇE¼î'°È]Pv–Îr&ž$E…ñOyÂAOñ¾dÒGuž`ØóÔI¿ËŠºe÷¸£xD^¾:tbÕXü¡¿`2*,jõåär*²Õqø¤Î@,EÈ3¡|z\j£L–ö9#áZIй]Q—l¢Æ¤tÇ#µ_¿ ¶h< 9g¬# A6VÁH¿ÀˆÏŒVzŒÕl–XÌVÜ™ZÇcØh1X6Þ³ œ[øe’°þ)o(à¥Z2é/žðÀIþ"ÔÜx.¿¾Ú yÜ>B‡Šéƒâf¬Š rŒ‘ÂçÀ†bI­:éƒoôÅÍ+W2]öÖ¡O¤[Åý°óëÈ-… o”m”!ZÏzŸâG²í ÄŽÄw[¨Ÿó2ê*=Î:]NE´IgBüðCì{T}mðŸô|ýÊðfò›\ÊØl:d±c¢.¯IµÏIb‰©òxÈ\Ï”¦°å™Ë}¾™þ¾Á²Ï—ù3¸ËFÞ øsSG›ØKª“°;­ÌÈ9ŸØÞpYͶ½WÏXó®rÚK‹îûé?Þk^9òªåS^ôy¢ï?½gn¬q‡åôɸxfûÌõã_Ÿ8eçúGŸÑË®êÊDZ¦í{¶Øâ…$è\Ú‡¤[Õ¨ÁJÚ»m@Õa_ˆ;îläIœã¼g9êÓ$øé‘w"fÄ ¼ÿXI„Ôȑד¯pý&®†Hˆ|ƒØÑÐ D˜ƒóœw`ÕrðóÎpV’(I ­÷ˆ0ÜÝ¤Š„!9XÿZ”ĉ WZÃù´œŽ6ÓÐâ|’ƒÿ¸+þPÊPeøÅW)úŠX©®àÐŽ” M¾åCd±Z`9GcÜ—ö-—2ü$VááM{öݾñ'?ùqãSW¼JµÅ¿½üá¼ÙöÓX´ºÝjnÇ*ÁV¯kãÞ;7îßwÇû…µ‹ÿóË#ʼnS]摈¤Ø¦IÉ\à^p/MV.Zï¾Kÿ`è1ýCš¶§ŸÓ íO«nN‰h”ÎÓ\¢¹Z³À½Ü½Fó˜æYÍv÷¯Úkÿ:¬1~(%¿ ¿YÕnj·u™ºl»¢»â‡¢‡âÊ É]–xl[¿c¦fy±ûh _ãº(øs–¥à—=,0‚s„¶!¬”/UnŽÂºbîì†Y¿üÜæ1«êÍþ¶ˆ/V|ë©w‹Pÿo'=(Ε¾lç¡HÄ—;úÏî½ï瑈ÖYó·ƒÚ~ójgÏàSÀø·ÇÂÀ¡wÚfÇô˜I=pLFvžDþ0ò‡ß‡¼xà†ÀòuÀ ½Ä_ hQ5ÕÈë€k àš¸æ®DebX!Ï ÅгI…^5HLa+Ç)¼…îYözf½[Ñ.…§ÅP~!j.žÇ‰{ceØ!3Ø‹HràÑišQ3+¾Œzà ^Fp6y0Ú‡ÕV†Me³µf³ÕغÿòŸQï.ÛÞRwaM"vÌëªÊ¥£þÁ={7ܾ÷'7=cõNëœNu¿|š'Œ£k2 ”úæÁ@¶ÒK·ïùé¦û!yãE€ñlìNuƒš~Þ6…, Ê%DÉ-Évp%—QX"pn‘ÀyÈŸ'(hoÙ¹ó–ïýð‡4ë6þ׆ë.¯ ¹®ñlùΈ-sŸûçà¡É›;]žâñüX“¨|bíš'Ÿ\³fû©ª;V¤'NNg}ým;Vý¯^<ÕÔ<Þj …â~Œ~³3À7›È«mãk1`œ|ŒÙPa`ÓÇÊbäkÔ~…Üà$Ô2<ÍÞn\‡Ø‚òÜ—F)ƒ\`¦D»zÀÁŒ} ÁùÂ,;À Íhu!ŸÏê¦ãzÚNC«Y¤òln»RˆÐöŒ7HÌ\lév€Éâ²?ÅÙ†sø"ÅgØcÙUGo¶òQ‰ä$,˜áVÔ;QÏ> Goðx`c!qý€Ž4ׂÞ4À‘ZÔ²;*qGJL†;™îÓˆœôô䥖Œ`æB¾ï8ä£ ¯Ì†…n–…ù‘l$MÙ;£Oœ[ðk%)V‚ÔÊk𙜹§…»“úû™ §J6ÇœâLvþ5×XÚY2šÒ[Aõ”“$ êÙÁ`Þ¨ì‰Å3“³‰ùDì——"9¤úh|žY“¸4]=#AÎÿ¤ã±Xñº¦øn<[R‚™Æb3ŸªùÛª½ØÕÞªY]z ¶§c¡ÙpÅ´0ê)¸:‘Ø— \dV›DdëÐiÊ+Ñš5 T1:”ð# 4F¹SAk‰• ¦ÉK;ÅFΉ¡}ËÜü‘ ¡Ýór‰ˆá5cÉ%s \„e•6•†À”®9á”Ǫ~}ë‘€4flæöaa x“1/=-Œ“^6õlØ0ËĽÀN;[¨%êåZ¿Ò¿éhW´k:e´ ™#›Cww›wØwè™Øè b\»@7Ípp_ZIè€B.Güc#>eɼ%¢mGŽ>øà‘^á‡Å÷>û´ø ú),{ùŽy`ë/èì·‹ŸSÃÛoS}ñsÀX £¡ß8˜ÿ¡íBè½a¤t_tß0R º¯Ô‡U¹1 p…ia ”}ˆÒ ðć‚oIذ"Â{¢@ÉW˜ÿNصàspžƒóØ!;7p>ˆ³ò(>èS N§œ/d©c±vLûåº.C‰²¯ÄÐ 7»J‘ Y —l¯á•§áõx¶ U˜bl\ìè.ø<dŠã≚±fóØšD’ö—¼J+†È+Wìm¥õ÷î»ýö=ÏÎùÃÐq¥ Ft뼎ŽâçÇŽ;;„g7Þö“=nßÃ`>0¿‹Û!µä=ø¦vcì?ÂèŸ$˜Åùòû—ƒó:1Ø› ˆ‹ÐL˜¶V…܇¸ÚPׇ ¼¯ ?EÀ_A<€x®‰Ù¶j$O0¡gçIh3Ì…9 ÔbVrè׃+&Ó™Œi<Ìcœ#€8ãfx oÆ;ÀÐþ/g•Xü:k£52£#\ö"œmdœË1k÷l„jãÏ ËFî]r¤8Dõÿuñö—÷x47ÆÌ|TñbG&ÒWn{æ'nûÉÞÁgè]Å#¿¦úŽŽyV V·ýß|9ûáÄû#Frøj𺹥-lŠãÜœÓAÖ2¼ÅH™/†á,ÃáaüÞHlGÚ$¢”á)ÃR æG_ÆÓnZ€”¥™^Ü ñ5ÁÒƒ~¨?ܧWb\ÙÈ9Rú!1Ì®¸ë¶°Bè$¦ŸºŸAK=Ó>/‹÷¹Ü—üúúYW5ìKSS~|9b†‡Ub¦Z-»¯_>¾)Ò4âê0ö½CŸIvŒ½@ç´Ý‡¾ËéRé òx[3,qYŽ´i Òv$xò"U€dÀãÓ¸—i]_£Æ‡²”D¹W ¤CñÅ@>‡?ø9“LÇ‘7ânDäã§G9EÂ^¦n˜ ²BÅW—»[a0”¼De¾ÂøuÙYdä\ä,ÇûÊ ®açQ9Ÿ»¹>W•L—\pÙœ¢×˵\úÈØÅ,ÆÝÉhí×Fbé ¸ ›±¸wûâ+âž@Î wN Í]à£S|ﱺT"?ëW Nã‡ú!³ƨžjcøÈ¸£ÌÏw˜Öë*QÂü´!Š&ߊúªsð±Á1wä;¬íùXâ‹^ûÝå·/¦BÑp pjš?Âìžö‰ Ïþ„þ‚I9woLIbè}éqÀ+J~ÐOÐÃæŒ€F™—Q^Æ /×DÖ#–Z!@+†û£€„peKÄ @JÌV#ƒÝƒÿÜ’,ÇV`ÐèÌ€4=ÌCf´36>–wããeQLÇX”¢óo¯-ãÛ)ŒVK<8EÿS8S Kœ:š¢ù1*Ñ@ëäúh­‹Çs£MŃÎ\&•qŠ\ÒûbˆÄX%ÜŠíl>æã:køñh:Ô¸¼¸4“ãm£ ©˜c¦Ûr9’„1†€3nŒ–Ùá-àXm™÷Õ‡T9í ”€XÃ’`×H€Y Z°(Z³õªfÜÇ|aaÜ¡ƒünFï r¨œ"gFNKX\ûX³È±7¡¤rò»k P†0ö6aœkð«%.ä˜ÆW‰³’=SÆðÒq³ÀK߃‹±{;…/;r/†a àŠò-Âܵêx<7üY•ŒÄ¢TSµUþæ§×jâ±pÂpÓGjl6\Kµ>_8ÿ‘. '+è`1‚ˆ…¼ðnß¾˜#$ɤSOÑýˆoÈç 3áÃGÌ¢~á”™1dƒgOÂ7.ɺ6m=àU”Yóõ·"m€%z+°oÊX,ë}ý.”íÇY ˆ )´ ²c 5Ø;À×Ç€fƒøŠãô0Rp h:Ž#kiÀLÌ@3‰•\Ì`‚80ƒmè³%Lf3­—̡̖£QªÄk€ÐKÁ?–@ª-ŵG+®$œ ¸žŠ4IDÒ|­·qÖ†YF)¦¿¼Ãdxr«1ø±¡’xˆ)€£—Ñxxç~ø‡Û:äuìŽÑHìÕ³»Ëdä÷ ×Õ0ú^À5S>”<5쪼ø­m§)n8ˆ™ÚJÌü,š,ñû2ê´ÐÝèÉ7æêŠýÞx}‹…¶QÑ\U3þX}®¡VùÎ1kc}¶žJ¾X¾Á]üˆ^¬Ž&³¯Õå³ÉÆÁM‘`8÷F‚Ø–[çã†q0üæ›P,óaoÞ[ÅÝÔÃè— _š Ÿo¾dÈómyÃ]ÚO"-â³Ê|hYéˆ=˜Á fÛ[žc蕘cfeHZeЊq·f6 ¾5¦1“PËÁÆüÁ[f(MÞéø1¶W wĦŠžõÇ›¶ê@x©WØ×U ¢á“ ,Cþ[žŠÿÄùÊó \© µðwÕd"±Ø»OïáÐÞÈ9 žZbck£M_%béÆ!=“k×t”ð:“œ¾+Ä@úÃË–¯€ö´€eŒ móáÙ!Ð=cv1P…,Äõ*œWG­‚Ü€üä™þù ò"ÏtÍ]È—ÖçL€¸ÉŒdB›2`ÅÀYÍà”1Ðtd͘f·ÄЊy5{=f‰¹vNÌò˜-6‹2й­-˜'&‘äà¥lU"€1äW‚þ ª€㘭~|H Ï5@íã;FP!y/£!tƒ#S{c•,¢)Ëbz"go¡,9ˆh`8Êç´HRÐMô À7X;úÔ“ÍÎj¡µˆh¿pxÌxÁÞ1®ªjSí x„vVŪӮlµÐRs!l¢Ý‰váO`Œ Wq£¸WfÞÜVNâƒîLj“À:FÂÖ2]“„<Ó±­ÀY¶’çÂÑJ5žZ’D>DúЂ­N3»ójôc$WÊhÚ¦Ía[-`õN|Н¿a¢\%)+,Œ±ñ Rüû ¥€¦áõ±úÓ_+à~Z¦$ ®SÓÍ:ÇÌêä®ÛG¯!SïÞyÇ®DÕL£zTn*=æÛ´sË⎠QÔv£O}º|Å<›<Û[)*B¿¸þ“ݱàXHµWnþaiMô4û56A§g~©)´Ù€z¬–D+¨Š­kaó™È½\OÝŒ1³ÜfÌ|3‰sâcÌû‰VìsÌgø@JØ|ö'*ÎΘÞÔ<}zsËùbã4䦵4Ÿ_\ÐÕÔ<íü–æéKKU,ËšœßÒÔÅÏÀç¡7éj°Í’4µ1Šg«…LÒÒ8¬=¶‚È(X –gRÔÀ”FîÙe¯ˆï~ vªäþçeDF¼æo2»Ã,½asÑ“SL§mŠÉnƒÞú{á°øªt³ZCVµ1ù\^Íôù0ô.'HÜ„çh ¡2ËȨ%Àm˜ßa¼-kéEM%Þ% ˆ–îaw°è ö†6è Ô܇z| aélm¦&°eo\ Š4°@| ¶¦äÿ¿×ˆsÇM…$¼§4(eJ‘6iu” (~Ÿo¯2¨eJAhÀ… |âÖ«"bè+Á*ÕøxMñª+•Ô0ërR؆oš<ÁhŸ¬\r ¶jÂøX䉱 æCªL˜çŸÑÌ™v”91zÆÎÜ1 ð!”߇Z+‡¨‰}QϹ9½c¬¬O–?ɾì5,ÎJŠæéŠKç3j€Ïï E£¦Ÿ+Ì(Ѽ+7j¹²A‡ôH×»õ% RlÑé¨hÁ§.%Œnù·RÀ SÉqø8ä¹óóKô­ÿ¤F%+%GsÅ£Ãç¡7‹‘mÇ5‹I)ýÃY®,¢±üþ¯_þúrïéšr2G:†¯fª£Oã,Gzé˜tLP¢ìa¤m¸~éRä—âŒkúΟ ¡6+#ìú Ò^$ÖÊÉë¥vüžv\³çŒFúé} _ìŹ i úæuáÌÚ²ëç‘ØóÆ#5!i‘H‘f–Ï-8ß…ÄîT$ MBÂû°÷'¯–ÎðL#·"±þç"±>Ø;FZ´i'ÒgH‹‘•ß‘åg#±wgïÊÞ‰“ÝÏÞ-vý8OCbï øQW)±wÂn•cä÷H'ô ~‹i”žî~!JâTñK©Cú>ö ý·L’-––O’ÿAq³â¤rò„*©Z§ú­ºR}•ú=ÍuZI{›®Bwc…¡âQýHýqCÐp§qŠqi¶écs­ùQK³å/Ö§miÛ3ö¤ý¨cªã7N“³Ãy»óñÊtå\» îÝžzÏ×Þ§}aߣþkü¿ <¼.øbèÆpmxoägÑIѾØ5ñ|bbâóäÓ©ÚÔóiKzRú·U 8ÖÌ¡÷€Þˆ$€3@‰Dñ©ú ?†m´YÂ:9Ó[gLk»pÖØÔ˜«W\wùÂëpÿ‡˜Ó•¥Ü·ŽspÍ$£Ô©Ópm©d¯Ñ3Ó›ønE;!E<à|%·d•F 1ÄÀÙº| |´ ô×Ë30;³Yüwì¯:´bo$V 02Ú¡1vðMÈð·:±æ7™LÁÊãTøG¦ÁWÒïô h“øÓ øûs°â×h…¹ð¦ï…ív mH¸™¶áíDZÀ‹t$)â8‚›Ùš&m‚_R¤¼¤çëчHëàÃi-/¯áåyxËDšå%Uü˜¦aœe4ɯ°ÏDÇXDÈ#–gþz‘Fx-k)ÒïÕO}À2õó2–)'‚žäƨdÔÃÛ±¼H]µH+yÞÉïpP;Î2~©¼Ä¯,¼ÎÌŸoÌDl04@ºË¨‘×°¼Hõ<¯åG ?ª© ó%ãG‘*É?0¿øì/pH¤ òßèI†s;®ä¼½Œ¥r;‰_‰ü(pˆR¦Õ?¬sm2~fmNAr³û™ýËò"d­a :x¿áï!ÈpfW,/‚&M8~E¾„R†3«ùм\ü’ü«½2^#âx3ÊþIN ?¯É?Û†€EÊø˜xÈó"þ®[Åü;ïo€ü -#üŠåEÒO>†V$Ù=ñ¯ä/¼Å_ùË‹ä3à¸H>ÅZ–H>抸ãÏ ¿Säy&ûžeðÄ™õó'~ü#Ã0òψz©ÕþÏ¿E‰ä]ò[‘wyË‹ä^ó6/y XÞ†ÞßâWÇùñÍÒœA{c3ÀæO$oðšßðãëÐHDòkÞË1ž—ÿŠü›kò+~Åò"y¹Ä‚²WyË‹°èŽò2vɆ餗Qþ–Æ/xÍËÐ’p5Äféåñ³‘cªˆoIüœÜ^Ÿç½>ÏgóçXiœ…2V#âÈfó0z¢ŒÕˆ8²¹d%"ÖJã>þÍL.?ã½ý”òqþËíðÒC¿AìNzÚ^þûxÍ>þ{ÉOø;°õì~Böðw`5"®Ø;ì)‰Õˆ´€£(v{ú–ñ#Vx™¢Ü^ÁaÀîÑ¢ôîlÇ ÓáØ›‰¼5åxC¸„ÅGI×o¢©ÿÿ#ÿÿzwöiþÿËeŽb endstream endobj 312 0 obj 19584 endobj 313 0 obj << /Type /FontDescriptor /Ascent 754 /CapHeight 670 /Descent -246 /Flags 33 /FontBBox [-22 -236 625 705] /FontName /VRAWYE+Courier /ItalicAngle 0 /StemV 0 /MaxWidth 823 /XHeight 503 /FontFile2 311 0 R >> endobj 314 0 obj [ 600 600 0 600 600 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 600 600 600 600 0 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 600 0 600 600 ] endobj 44 0 obj << /Type /Font /Subtype /TrueType /BaseFont /VRAWYE+Courier /FontDescriptor 313 0 R /Widths 314 0 R /FirstChar 32 /LastChar 211 /Encoding /MacRomanEncoding >> endobj 315 0 obj << /Length 316 0 R /Length1 38056 /Filter /FlateDecode >> stream xŒ¼ `TÕ¹8~νwî}îìûÌ}&“df’™$¹!$,%ŒEÀ•$"•’Ö%­D[¬|­ŠKkX ®Ñ§¶.Tl­m­Ø‡Öª±´EŸ-dæ÷3ñ½÷ûÿþ¹¹÷,÷ÞsÏòíßwa„ !É]±²Ÿfû æ r^´a½THÿñ8Bø„„÷V÷¯¹bøØÃ]CjK Œ•üqp*à¹ÿíO8«R‰TH4Ð&ùƒ¯!=ÍÁ'‘ˆŒÈ%3² +²!;rLß;;q"r#ò"òCŸ(·C(Œ"(Šb£*CITŽ*P%J¡´â6èÜBä‡ÓÃþÞFÅá<ç'…ÅSŠËP¨piñ(k†·aÆè‰ ½»Ðh÷8Πa ‡Pê@?BsÑ›è èù&ü:Œ<„æ ](‚ýˆA­ÈŽh;zÑGè(ô¦ ý› Ô£Êÿ×6tsñ <¥FÍèè)|9^ =mFó˜rœ„/o+NÀøãÅCÅ#Pú ú‡‹»Ñ<È} ³C[Ðí0W—¢×Ч ¿at!z_‡ÿ³Ñ‡ná²ÜHñ24íG¿Ãm[„6)ލö£Ëá­Ÿb;ž(~Pü3zŽÃèbhéûèfèñ4ÁT²ÍŠ0§Q4ƒVÂÝkѻ،3¬\Œg·CíÃèïL’y… I4­@?@Àl¼ƒŽ¡/±çðOðcp¼…¿P¾µ¡«Ñ5€?Ù{=Žâ Î0vƳe‡•ê‚{ÛЃðý½è0nÃ=x¿À>¨H‹–¢µøçbV³z¸½ß8Óð | ²ë9·^Q5õ=á*t:ŒÞ‚~üæýKô5.ƒãCæ»Ì–âÒâ®âGÐ%@L:-CëÐôôo°ª/¢—ÐßðIFO¾É½¬¸Fq¼xÌm͆¾·ÃÓ‹¡í[`•ö q8ÞQ±£¨Ãçàóð¼ ß…Çñ»ø]†gÌó);ƾÎþ«Q(ŠõÐ’Bj-Eka¾ ³}Œwz½Š­8Š+`DïÀû_13˜9pü”y“ù#{#»;¥¸©p´ðYádq esa®FÂ,üÛ  |)¾ ÿ'ô|”ÙÇêY‘ ±9¶‰íd{؛ٱ¿bÍ rqï)æ+V*V®,¼Ul+Þs[}Iå(‹j~V4]ýë‡c]‡¾‡FÐm/w è1÷óèUô;ô>úVáôùøúu7âÛàØŽÇ/à—ñ«øCü9˜ q¦†idš™Vf s#?b3ï0Ÿ°ö"v ;Çýìö]qWTTÁ1Oq‹âaþu!.Ì.T¾qjrªlªgêTp–î*¼PøsqIqô?Bñû:4 ½Ü0ø $@¯ÍýÚ׿c+â8ÐP«Öˆçâùp,ÂçÂÑÇR¼ Ž•øB¼Ž-x_oÀ?ÀwÒãÛƒø|Ž'ñSpü€?ÆŸâ¿3Ä ÐabLŠÉÃH›™¹L;sk˜upô3ƒÌX¡‡™½ÌAæÖÌFØ v%;ÀngÁ¾È¾Íþ“c¸r.Å5pK¸5ÜõÜ›Ü[Üî¤Â¯hQ¬Uܯx‘wóY¾‹¿”¿‡‚ÿ„?%ðB‡p¡pð¶PTF€ZýƽÖô›¿ÿ&¾Jaá62^8Ø~Å0î‚ã™Nörö6ö7ŠÕø8+á÷ð{ {Yñ§l+ó5»/ažÇAÖ¯¨gW£[Q?Æ|Èœ`þÌYq'óçnÇO2ëØf†RzÅo9+w½â„˜ÿ@õÌf<Á¼Ì^Ï^_|Õ+îÇ(îgÞBw”1£«‡™»¡ƒ¿f.anAÝ\Vq]óþˆb#Ì÷,æf\ƾÍÝ>bCÌ?ðq|PCxf.`òø1 ¸S؇&ñêÇw"?ßÇãã]ìÃx!£…Õct¸#tˆ à·Y5ê!Ó£Œw0Ç™.öþ0›Ã¨ÄoÐ5˜Åi€òÓt%`À˜д &¿ÅUÀ}îz¢ð ¡ØŠ#Š[Î`ËÑy(z™×Q=àÆGpt£›Pz `ðf”fîAׇð* û‹€~2h_ŠRXÔÒ}ÛüÂÆ®€O ôÿ5 úmø ô,fM 8GîÜʵeêú{ «P/”îCwðû¿EíØ W*ÜPþtðœÿ„ï»Pôoz€+‡^K@™àû ó ÇMèuÌ ÍÐçY€çÜ< ¼w/…^ó$¥ÔPQ.µ„¤±CsBÒ8^vn7ä0'Ô#MÒü"š¥yäxAjq¬#á>©e¬uÃÚ‘–¾9åx·FÝj¾X]QŽv«5Õ@nÌêßí³0Í0ö–úÝ Rê`ˆc®Ðœ–1g^…fØHËÊUcçv·Ìq=åc¸ù¢Ð…c(4{̤ fú™1¾yL Ÿ‘.ƒÑ [¤Ýå#·Ž‹è¾¤vUhÕÊó»ÇØ•ÐF˘1 ß3f¿æ˜ã›"4njî>û®›iq\"‘‡GF†¥±çvŸõ®;@Zèé6ƘHkßH+|øVX§¶Å|‹¹±§{ ß”È8ȘJ£»8ÔBjú.•ÆT¡Ù¡µ#—öÁ¸FÆÐy›{\.ù`ñ(rµH#Ý¡ÀX£;Ô³rŽg·œ·i¯S–œß¾SQ¾[4–¦u·Þ0ÑêÎÎ\ S^ºGsôq’k;ï̼bÒ£Ðü1àé" zÒ‚1Õ‘ËÅuhä¢:˜~øëÁðÖØ*XKÆTÍ}#b=Ô‹0D<¦ˆˆ!iäKëšüüÛ5+§køˆø%"7 ”œ´1`rÓ@7–LŽ••šaE¡³h9WQ¾aœ õ‹$0}¨æveO} &? Ë{˸Œ.„ÂØÐ¹Ý¥²„.tïAr*Ù3Æô‘;§ïX»È¡ÓwμÞ8ÞGµ ë˜2zæß ÚÌ-këǰíÿãöÅ¥ûm‹Cmç.ë–ZFú¦a¶­ó[¥Ò}2¡0opo:‡K/„q‘1>2? wÞ2€£ùWDZC-—ôÍTƒ>Ž™›»Y7 ãfiS¿ç/;Ý)tkI[\„§ð¿j\PÓ,µŽ‰}óJ×u 0^ÿ¯—Æ‹ÇÉ[4ùæµé1Õ'§GUãØŒo•¿Õ=íÛÖ Ô‰ië\62¢þÖ½V {##­!©u¤odåxqèÂ$†F²Ýl÷H P¬ÒòŸºÅ=Özk e-® gÐìÝ!|ó¹»e|óâeÝE„¤›;»÷0˜iî›ÝCæ‹iîìžî/yè1Y `ˆÁÐÈ< Ú£€ífðÓÌs Ìó{‚gžÛÇ"µ@2û1r*yÅóptMœ@*|¾9’âW S çˆ'M5 FÈ‹§à’IŒc.ØÃ¡S;qJV “ …LÀw1ê,,`®}ÐŒêåÐ]ƇÌMÚ­FF}ʈîM!µj—>ØÁc~ÈÒyùLïäTCƒߘlœÌçǽØE™œˆj­<ÏX-vÃ\w÷Å£÷᪯®½ÿœ€kÁæÂºÈÂÕ·ã‘·q .^Y6çóÂ]/¿óÄÈÃ÷B*¡Khòr8Á•)ç)Xø¸:aF¥†H|š—y–²vÿìv÷šs6»Íd‘«©1å²±J¦òž‹·ÝWxó¿®Ý±(àl»N±ª¬mõ…ïü®ðZ_iù _öòïÆFº—ÎÕ…ÇÀñ+ÐÀ˱¦Çþ’UÙûœ‡¬ #ã J:`’µ®Þ`õ[‡¬¬u—É¿a…18÷A·`ö{MõNÂÔ3å±ÑdÏgÒ0=fèô) |(Íekª«lV åš• h"&K¦¾­föšm…ÇʃÛ:Ì:•EU_i½jÅšÝÐ=˜£Û‹Çð:y4(){ÌkXY%×çTrcn… ïP=¡bT7j/½†ÌÍÀ`2IúIGèWJ_Ä(%7UV65½H¯•)™´Ë1³`îYtž¬BŠ×ýkj`ÊÇÙ˜¬cX ð,N Hm~Ù"±i¶ígw²GYž}ÿœyÇëv@¾:y‚ ¼¡±aXQ™Ü,¾”I'1(̬‚µ¦¸í_K’±0h Æ2ÊŽÁXì€5¬sGd¯nMͨs§“áe$he“Æ [aˆÙQëN+c}G@úü @#]û“"!îMâ³i>+0X2àòTÓl’²c¥‘W6M™iEeålÒŸ•…V¡æ`6êÄïË—>„jú¼‰ENäVO‡³Ë³Á&`Å?AŸ6]òU ×Ýñõ!Ûá%œÔ!+·ÂÁ„%FjçÖ¢‹™5ÞaÄm}ñd»[Ù4{võlÔ~^fvƒ8 ç*koªf¸f7ggË*qžµ5ãf(=9ÛÐ z¶àyš ßw³s÷/ü^¯Õ>Ξ+×­•Ùõyk¸ºL¦k‰¦µ¬ÑõsÉvËnÖíZ’¯3ÌšÏÌße®—‚é ìrAg×ÐÄ÷î»À1ŽkoL&Ï=ðxÑä‰I£)Ÿšú5ž˜‚IúXü¨±qRü²wª÷#½Ø”' <$¾:,êš4o’gÌi«©HÏ×:¯eËϨo¨gøò¨*bJc$GtÑ93ç¯Gmµó½ˆOq^¤¬Ð¬Ç6¿y_½9¼.HŸÄ·Ó%FHìEú<1¯¾y=^P·Ð‹iÁ‹ÔIa=²ìô-§§”šB(ïÇÚ„a=FÉé?œLŠ@”NKiYÙ÷¾÷=<ÐKþêêPïfyÞj±UWÁ€(̵2qÕ2W3( 3¡ê*Îdµ0%L­åK©©–ଽV¦©!òuT1tu“')Í?tǃ…ßøsaýŸ_Çýoc?²¾~Y!Zxë‹ÂÚ?}Ÿ?ù&^ô‹ŸžÚºp‘éG{æÌ½òÙû®ZÞÜ#^l[4Ð1cnyýЭRÝ|ö¹ÂÀÑa©üL& y}>J¯v«l»Û¦d·Òj´¸­}Q}OÐ(ZŒýFL”)ý8óªld|@>}~ïƒt2qãx¿¬UŠÖj³)•*¥gÿKVàæA½lgÒ{Û}Ø7Î|.ë%Yß¡?®gõK—PšçtÖ:\â¤HÉ,öÆà7 âTð¾2©’;Ü«¯t$‡›_J:8‰Å‰ÿy7¿Ô 4À?ÏÞ3ƒ{{p€B¶ÝÈh×âjl³Ña^†}dêçg\XèêrVÏÂï‡ð‘|ïâ©¿œ›_ùñçø•wÚcþ”‰ér矼çæs‘W(_uLx Ìê,輈ûìÎ>°+Ô1›åô2´Ì·ÝìÛZ½Ýõ“Øã®Çcq}ûsJ[‡®‰mª¾·j{õƒáG«¸ŽÄŽÄÕ\ý8ó罆55õh<Á,Iåÿ´Ú³Õr .N_¶JÅáâöfç„çD¶ºÞÅï„ß«þ("paÑU‰¬•w»,>[Ø·¦+«Z ²Kq·sYì.Æ("±¾ / ÷Õ÷×Õï¬WºÒ®ªÄŠ‚+ì‹;Sϰ>»¯½úæð½áw«©^®ï¨¿ˆ¹ˆíSôñ}B_z•ë*w¿o}øªØ5ñø›Ü7ù¶UÕ¿–z/õYø_agÒàw«AÑï¶BÕaÄ‚"—ô‡Ù`¢®¼š­ Æs9•-·ÛmLeœ@ÊhG ®Ôçh2›$C{›²¤¸·¹•¦²ê®ð`µ/ía<]\Ò_Wž!Ó#¶äL2·“;ʱ©TëŒYÄa‰ÃÀÂß’#å¼ÙÌt•k rÕéàX6ˆL—A"EÃýùúgð[`Q_ v a%ÉdâI€ ©Þdï@s÷A”a+þâ¦ÉddOô’G’ÉAÄIrž3IÙ °–’˜”LRÖҔʆâ\n§›áùh8ÂDª£qG´§„L5ù¢ÕlgªÙ˜;QÓŠÊjñ«‘¯ŠÍUƒ- XaÀJ„|åDƒ”_›`@êÅ 2‚Ƈ¹ê* åF"¡…r*»ÔGl„Q b›‘ä! y^`÷ü uåÐM UwEìÞØ¢jfÁÏ.ºëþ릮¬ÈßñÃs^|jUÇúýÏ-yqÛ¬n7³Ï7ûü/>Ø© ²—7Pq„ŸüÎê ‚ÐøýEßÙe;¹ÎýÓíwtr `´ ø¡Â´:Œy¶Ê—Â)&Ŧüw¶û~jø©é€áI“FéƒÞãÍìµÖ¶°#¶Ÿ°w¹gŸfUZVÏ1Þy`ðV¤”¢1 RVìgÜ?òFÛé^EÜÃâqæƒý`Œ±8Î6íߦۡctãlJNYTÌã â*ññ'ŒØol42F— ¨jØàð;ÇüȪ‹¨Ì›ì\EïWƒ R €h65p¢÷ÄÇ“ŸŸ"R†ø*]^ÉêæµBÄÕDmÞ­ª@Z+\”NEVÛu  Y9Ê·aÁz±9D'0j²µvž I1¡MájX)²rµÜ[~ÿ¬~oó†É{nxm“uáøÓ…'ŽÀÏþp[™Émqi—ªß<°µðöã…¿ì²ìßõ¯§N½Ž;Ÿžg3»Ó%Ù4|rÐ' `VîѸ5Þ›Ä;Å߉Рâ˰xy»õU÷«Þ·E¥Ãh²x}¬`Åî›}L\ÉûÝ C~·.²œþ¸^¯cœq› )= í&ŒL¢I2¥M²Ia/þñÁ*ÓüÁÆY99„¥îí  ±¡€â£â£N¸$­øÈÓJÞEÞçﮜ^‚SÚ ‹Ä^P¾¢Ëò ÒåóÓHæqù V1b‰ú ž%Øe…‹×è_‚Ýfç’Ó ð½ïii°w úÛ¨!d$ | óŽ€Zf„ª—„m‚qðÌ|áñ Wÿ~Ë’OpUá×Ç—]© \Å^¾E*Œžûmá£ç޾Ѓ[ÁáÄs¼dÎ1šðþ(À{÷Êj®µ’qÆ\qFtˆNFª‘kúj6*ûýÎe£ŽQç˜cÌ©©HmÐ kXGM¥«£¦¿æVîçÜÑNËÞ¤™¨aç)}~·ãA“ßm„²öR À{µÉÍ™{ËíG—³úxP…“~Ÿ–?YOfÖ4;L£&Æ`j71dí¶˜Š&ÎÄ‘%0ÁÛGpœùZÖ¨:¢ØõG ÉÇe‘PѨHîGççVÕ0Idê«d hå °m²TǨô°A<+Ó«”•’‚¨ŒÄc‰XYŒåµ@ ã ,ùE£TW ].¢¤ŸT1¾k"ú Bݾ„Ë’‰€Ô JˆA% paò%\2‚– X‰^j5ò| ±¡jˆ¬KÑû ÄÎMϦ†îúÇPÛ­Mþ¦óó¯åª£[ ßycû’Õ{î|}Á¦uuf³›$ëÜyîÕ‡~þ× wF#øæÕh4¹¢°rVý©gÿkïÏþý’¥Ž„5TM×®ÌFÀ7?zA^ÉÜd2o9žsVWÕ(ýn&tøÝ¦@Ðéwã@Håw!“‘a°ÒádÈÊ9•dÊyÕTõ+‡”G•lQ‰ÓÊeŸ’]¡œPV²JŽ<¦¤¨£/~½¼ ™‚ì%ŸV®”úC£6èô؉Àá³ò°~°fd a`õJˆFÕ]Rº.üE@n:ÃH`¢¿™Y‹Ì&³qêétgÔ¡SûËÓi¦%³8êÔ©¥d:‰d¤kØËל&ÍŸúÍ—ðd&þï0Giüªü‰ÁõHi×;uqCÂPÆ¥ÓL<3ÕãX‡×:®HmrÜïM½îxÏñ þÌ¡Ó9€°òéÖ4[ã¨IÏu°¶tÌM³¼C‘¶ÛÙ$J@iª·ç9g.ÝXÕ^µüL›œëÓ#h«ãÆôvtwúôPzgÕXÕöWU°¿ë8\5iÿÔñ©óhÕWè_öÿJGæáùöÖÔ2Üc_’ºÔ¾ÑùŠãåô;ŽwÒ9>JëKäw»ÁJ¿;2~·2*É@¿;a r8v:D†ž•NYÒ{:å}·»œN;£R*J§cqez9@3U”¤ÀÎÀX€¬ÖѸ_®ÂU˜!MèDƒd0é%C—ÖÈ€D~ÕK2 Æ|ªëâô´H )(¾Æü°rZ¨V‚LM¤kyȱ9  ^0tÊî”hÑ6âÒEÌ;ƼC4å‘Ò‘·ï·çíiKžÊð6 hÙ#€ÕÕgƒ AEŒOcã·oc¶uê„;Ò‘.ÄÓ@o-ú¶Å T|Žá¡ÔR ¿‘ŽÔÔDziÈ6õ%wõ© ›ýe‘HVd7,‹{c‘“¿çhñÔÈ™#'oÚ[ü¨ø)ÐÞ…(†_ÛFLØ´ ƒÔÑžÛÆ`“—Á1¦Â\gÞh¾‡ù€)2‚94Áš©AX3w£ Ɇ,d]C&“3Lд˜LÁà8þ7Ù{«U*̸]J“Š¥ë¡5-6%1-Ê"+Žî3Ââ@æÄ>‚˜$CÙ¢x‚J®ÀXJà‰£ &a¶%µé žâ å† ¥Â›ABvÕûƒÎøÊ›&¸€¬°Ôt¹)Gì…òÇÄ@J,U°â““Ã¥eF`°ËÓ%DPPï`s·W™œ¦nDyS;Z`Z–™Ö¡KMט~ îï§ñ~Óëø_ØôWp™íAI< q1Å]{}¦FưצkfñÉ*Ù“'Ù=Ó‰›–8ó@ÅHöˆl0åM6Sž­p:óf¨Û£ÉC3‡KÉ×û-yF6æ ¡'§Å\U¨— ša§)zè¿CY”07îggˆÁG,…O}ßmÀ"€4cæ ï ÅÂS«? *'·rsN={ºÄ>ÑRnVab(ßÞ|[ ÞÒÝrænÓ.áõ#"÷¼IÆ7 \³RG¬5Ϋ ~6Å‚ÁZd‰Qfì|/YaWcNòÊ^ÆklU’Š1¨ü`Éœï™-‰d¹HH~E2Ó&?0AUa·!¢‰º¢æ¨^k¬@nì¨Àr6äDµ®;¸˜”Ö dçàröt%A#èÅ`äZ[Cd+£H8¸CÁ–= –ë ×@¨É'…ëÿðü¸rëmWì}þŸ[¯·®ðváõÂZˆñhÀÍoìž?¼«ðLaß^pØã&|þc7“¹!zn’òµr¼ñ ª„¡þ°>—ª¼Ú±Þ½Þs]¼¿òN°Éñdø©øïÝ¿÷¼æ1±2ÍGò±ñt岨%±þÊ¡JÍ+»< O›ç?œ¿w+vÅñkáwíï…ßmø³0ï‘CÞ¸ROˆiûÝB ¤Ö!¯T^æ7†ÚCL($XË@ µ2JŒØ.tZÙÕïR¸æW’%ÙUb¹r¬’ÙQ9Qy¸’­,ÇTúÄTÂTúÄAƒžâ›žVê) Õß_Q9Ž¿³7@dPªþ7´wÑ £%­’ÉÊ3K:`ï¤)Ÿ7XZRþ<á„ÝãˆÄ£ ;¨{a\bβjqƒ¬0 ë —ÎïÜ$‹> @¡\Ð'Í€%ô#Lˆ7WbíCƒ îB&ÿ;ôŸÖï¨zVñÕðJZ€æ‰.ÊN=]½$bqƒ8‹ÿvà7£¿ÿUf°)wžwíÝónè¬î`®-\=ä/DêüëÙËI®mÏ5ÖÏU«ê¾»ÄË1ÿ† Ne!xø³ #ï ;ŠÐibÓÑ—C/W°óÃW0¿½ru\ ªH4ÑL"dÂ×âk™«üWI‚##xXº§¢»"OFŸ©(†­¼t¾5|CìÞðƒøgÌCá'*ž¯8’þkE±B‘UØÅ˜â°¾™úÊúôêð%)uXð<ØêwA‰»ˆSú@Èæw{!™)„ÃAƒ© ‡g$F(K<(¨°“N "Öô 쨰S`ä~Ü“Ƿˆª¸×ëaÀÒ·ÒDíxÝ%“\K{ž0íÀ™À~±Ë Ÿ®ak²J SJ:J SÊ ÍJaÊJ+­¦¬÷çVÄN°0P™ù4@‰½ƒ'ÀÌ$ð”*Á$ž¦÷$xLùÞÁT|_ `Zž»˜ƒ¸"ÀŒíø¤±ä°ö±LÚdÄS‘ñ…ü‘ŠPªg|p© –W£P8-UUƒi ŠX¢A%º(¥í‘âÑ=Ú<~³Ç’ ;4ˆ5dïóiÑä™fDX}2`bBŽýO†O biGFÉÈâ¸b-„våª%OôDæ(XZ‰–…¿8rhÛOÃŽ¾‘u§fš=ª_Þq=X ®Ëla÷³ñ‘«7G ×ÞÔ­e~„w}ËPŒ†Šr  ÛuÌRÙiº³°Ñ°ÈÀA¤¨"ÙŽÛ•±~·Ê‡kêj\¬›[áXá\áZáæ:…•MÔsë5ëuëõ ý¾~ª?½Uy“fX7¬¿Á0œÜÅíªMºj]V—óV{³Þ1aTp’Oò'`:œÅ4rigÚ—ö§3³3sótóÊ:5KtKÅ%‰%I°Eûwµ?ç®étt:;]=UçWŸŸ=?w~ͲZ=«Ñ$Ìw"¤‘êg$Òõƒ¦AóÖð=Â=©íé]©‰ø e¯$'ê×[ÎQÖ¹Ñ:Æý~¬¯[ð´DÖåîÍxÜÞu~·Ï÷”l"rÖy¯¥ ´1­Þ¢Õê“Ú2=UÑ„á)¾ã6'–,û‚Y0 ƒÉbÊø¼‘ùÂO?0²`Ä~Òÿ¸/)F“ü;*ñó•­,Q•çæäÊ7¡À¢J©2 ¤–«|·¢<¨ÃÄ F¼‡½É`tƒ'ÀE3085˜OQG^ã$µš±ÌfÄÚ«'ò(:£3Ò\/ÀÖB--5á´`ŽG5åªj”0rj†‹†¢ºB[4ÚòdLâjÐ'Ê"& °ÊO`¾äE¡Ô´$\ôì÷‚0¤ºH³Z·F¼(Éõö€Os0 UTÖj†<—6ä«á$Œ¶C• XÊÀlg0ÓBÁXÉv2Vû˜˜Ç¢áhÉZR5ÙÇ"¦ÞÇÏ_{srÖ_ž»¥í¯ÏÌÈúÿÝåô‚IÙÕ½ÿòÍ·×ÖÇ ?ûá£?¿|SÝP/Nï¼`˹³ªÛ6¯¾âGçÞûJѳ·î¸½ï†eU«Ë}ÿ¾þÖÎ;~›súS@šög_£|ùor=„92˼Ë|—á˘˼—ù”©@c =pân÷.ÅCnÁ^J1Ó Ž¸ODƒ20ÎLÈf° Ù®o4 Ÿ€P¶q&.»”*JéT”¨©(¥Sí6ÒG«ž¼|¢o…o§ó=ÅÄ‘­ø¹¬!ú§R@´¾WZÕKìþÉä ˜ûƒÈæ_MŽ4°GcÈÂ'‰ Ô„@îƒëVÖäà<}ëcÊf§ÀŠÅW‰õè%ýôÒÿF‰ˆöÏ !3÷€!ª1û×t>ò`jê"þtE<»@ˆŠŠ……;Ãõµ'Oœ9­Þ|ùù&”Ì«¦xT±æµ_¥Aì-KeÓÐÛ½R˜¦r§Í“óõüB~“‹„"±ªPU¬%Ô{0&$bùÓ‘^¯¹ÖpoìùØ×Q¾A_Òúý~·3,£Z¿Œ:¨†À«˜H\§*áoûȼAæcª@Ð ÑDSU*¥¬Í+ÁI.)ÓJý²ÑbEŸš{”òe³3uâýƒèt™À> b,‘B%œ®$-ÑB¨ß2“n;wÓîZ%0¦h ®6¡Ã"±HX/©ÇÕ&ÀΣˆ‘ ×À…¬naSÉ$عÁÊ Ví‚¶øŒM‡˜·÷DÁ¸ý-ƒÅÁi“7û>ZÝ‘´ž;ùÆ?NK-Äœí ;½ ·­½ñ7‹€ë(b‘H³`ê½7>|àÞï÷|ɘ6Ÿ‰äƒS»Ûß\°~ÿ&&>€Øò~Að‹1íSx?Sò5î³aŸæ?=©÷36AÂñ 6ŠS‡OàqjMbÛ”šü#6L% GÉX+¹ËS4•¯—BÙ˜NúاìO»Æÿ8w=£8À@}˜DxÔú°MñcaÔ0jº×6P\b]e_ÏmRËlKí‹ùKÅr¡G¹\}¾Çª¨“]ªXÌ+¤@–«³¶¢ùzE„OqeÜ·)@Ð ¤Á(t8 ØÍSªé’Úæ²•ÙX› #Ctëy J¿ž!Ø+N½üòËàïèÊÏ»e R`7ó«Û WÂÃ~»Ïí/ËF›ÀKJA‰È‚ç CP ”ì~ˆZˆxÕI;¶ÿ9m“m£¶ã6ÎöIÚ*[;¬cÖãV…dí³öC\ gg>; î Ï!^ç‰Þc½È1­ñÒø Â? NÃAÉÿÝYØC}„ÔN/|ÈÕƒÄð¡R;Lyƒlʃ?é“b^©4çAtÊ'•嚸6nŽ»î²x,SÃç]Ùô\~ŽÐ¦™çîä»…neº[ÛíêNwf.áW —kÖºÖº/«ÞÀmà7Ô5×j¯umtoöl”®NÝÈݪñÜœº9½5s‡°]óCóÛ]÷¸¿3õ£ô.壪G5ºv¹ñ<ê}8µWØ«|R=îÚ—þeúŸÊjNyÿ)-X›º8½6³UÅÕ¹/÷­ó_YÁ],\¬\«bÛT ýóâm)®Ç½4unší:”Ë4,'@€Fã±¥Ê< FÈkTÓpïE¦õî´ÊÃiŒ¥™u›”‚k”ù˜ÊÁšÖHüÒúø©ØB€¿\åñ(U*µ¤/ŸO‰x@³Ëâ6ÇS wܤ5ºM1_ÔËgêÜùñbÿ^·F-×É–´R´Mü¶n·Ëãñ©Ôjj r{ Â“ò*•Ab+L§2¼ ;žtг)ƒ’‰Z­T ª÷ó‚Sthœ#¾Qp©Ri´"Mg†2£¶=³"Ó—é§…£™ãeæåŸUçiÜû]š§ ¹Àk¯‘µÚÃZVûpýŒqæÒ½%TƒÈ¸cNñ˜Cœ:AU•äaÃ%í„&"Žaýæî}“QN×´LþßÑñlo¾á8J8ÀZE°ô4Ž ª20‚¢–xÌO>r‘Òpñ;LšFúQMz°œ™!§q²d±¦HiŽFÒã¬ÊiL å„͹Ù>K²pS,!‡Â…+*´––ø+G®®k>ŒK Ë™Ns‚ÃuÙ Ìa¦Ük‹ÎŽfC7œ|š½èÔO¸ÕßµGÁö†¾;%0Ã˫¢fIÉCU¢zË”Ÿù캴̯4¿8ÉneŸ€Øþ™ìüé©‘z ebÈ·º…ʈR£!Ú6aŤ­&vAÉÄtUÛÈ#Pþ# sB¶¶]MŸ­Î 4À¢B€¤‚WÀ¹ìãåé¬VVA£ZÙë%W#ÜÒŽß–}ä!­–ÛâÀZë O8ĈOh(çP BÔ`={Á¿CHà¡Ôáo'áHUrbâýdò%ñíCÄ…à–×i<#ÕŒiq 6IþüPã.Õ5kJš6£ÍÕ7¡[4·äx¯ÉV/65r*ÏBÅB¾Ej .¬—·z•j½ ¡à|ܦž¯™Ÿk«m®Ÿ?s©fæFÕ ê4†NÛõ6Æß¸¢‘éSV£lCe¢"û4  i‹Tym\“‡aM®>'|3Èû´¬D“ ZNÛà FÊ„&ßîXáXç`SŽ-à þ®¼È0âtƒÜÀÀ°û+†*˜ŠÌÛ8Û*9MåD®è‹ jV›ÍÂÄŸ‚໪ŸÆkìR$_ÔçQÄŠŒF89r< EpD$Ežfš!$Ö ¨êÏCÄåÙçNå3‚¬ÏK`› 4ppÝæYÍW–Ô¨ÁÁ$‰nK‚ìE|¡ )O# „½ñ÷ÄÔ±^qr qr„.óä™d2U"o{X-Øs{H˜Y.ªOÍÍÍð„æÚºš:†W)ÕJ†¥ Ãç4y° zÍd2ü:†f(òT§ÌJ8—Õ˜<¢ëƒp©çGdáMbÿŒ?Àï>õsŠh³®·FŠ^Ínànb‡¹‡ØÇ”Â\×+-1]“Ùg™ã°kç¶! Ïô$ãWŒ*˜>Åâ «øLkƒØÙ°V+ê:týºQ7—1‹t¢NÒ¥!;¡;¬t€ÿO6ät}‘ÛJ8@ êB66¡<´§ƒF{þËÉSøKŠq§Äj„¨Äú$ìR;<ÈéÐh=J(ù¹€„7Äòn‰„wPЧ ˜r”ƒodÃM!1F½ÒA!©6O[h‰ŽŠgÜxï~óo·<Öñàƒäð”é±¹¢úŠüòŸüdU.g¾:ø··NÜ9T_Ïî¿ožK õOŧþPUý«çÇžu[@7iZü#€¿Ü£äði¸¾VA¹o‹TB_ Œ˜0%û ë€Ãâí}fÐ!óÚÂS¼ˆ<ðdoãK“P‘¨ÇÝ&ÕqUYE…ÈêÙuKŒÇÜÉ-ý¢Sèvw{„5Š Š!4Øç~Y:,E)Tµ°s‰£Ë³"Ôçèólp zFL·™G£Ž‡ÀØûDh/ì(ý¥ðKç_”Ç<ŸJ'°ƒg˜–šnñß" …އ£„Ÿ-<œ~ °'œà4ÀE_`(À €: ql÷FÁ@{Úcz<  ¬ö~fÀ_Ú"*†wl›$‘ëLy¤&ð†_‹ÛµÛ´Œ6%Âv?õÁ¶ØQ4†"©`УW¹®w1.¼Ã…!¢¸ó°«VäKÁû ¾9Ø|¹½dê"±B½ƒS½Ç(X%“““`CûqùT%¥k‡’%ò–œ8ê"éLoï¡Fñ×ø>¡Ÿ`À%jîÜl –Hž !¿}©ë¸ë#ÜPê‰ÔDJSC)¥leÖd—¢KÙ™¼K€ÍÏXJժ窗¨ïá.Û™&RÇ“Œ$!)ðÀ; ä–©]º@Z­¾\ºFÚvH …WÊ4Q¥9¦m2ùÌs¬Þ˜­ÉãóÎ_Jh¸r+59./÷³?Ò´°ípl²öÙ†lOØX?hÚŒí³D}ݯ̒ôɹ9¾¹²yË´‹ê{a¯ ùr† R¤•J(]Ñ$§ŒE¢Ê„„’\âBDÂeŠrJ‰á†Ä]ˆS‡ø+ŸÛš0]xƒx·ig¡%–lW„rFbl†bæ—ÍC î:úõ¿ojéJ갱°¹+4…ã•|ÃE©î–åc—/_Ó:óäË/㹋ù %•'ß`®Çx™ÓŸo_û«×þƒÂôB ™‹a¯†yÙÍÓ0WÚ€çiIh(o $zJ4õÖ´Œ°äAp[}‹”^’Œl$ž~„4îˆQ@àXbˆ›iy›döº œŠïÐ7 óÚ“¸ŒF DH,ÐXðCXrêÐÄ7 ÙkB;$±¤ 2bK(}QIZaÄ¢ cl´êñq§À wpÿÆí°Xø”C#¸%n±ø}0N’…Ñà“ÑB¢·‘*½Þïû6OB8<ôµ÷%0ãWѾBO Àƒ—e…£×Ù‡ú,ï° §äaÍ“·A8Ø~ ·yAVé'l‚÷ÆãYZ½¸¬2ëæªnó¶öeŽå.³*^P)µ ë|~+s+?¬oôþ”y̱ßü6ó®á=ñóÖl‚@de?Œn«êáW†ãp;Awê¦ð€) jT­Ì\U»¿“éT];ò·š·:·›¦ú™z\¹_5¦þ%ógæ¨ö„Ú¢<,Àþ§Ã3@R2wÄ8jãf΂Ò6+âVX·XwX?û“Õý[U A jÚS2ÉóÀTs|¾ÞPÚâî¼Á†×ٶضí„Å2D¶F•LZ¹Mù’•2oõ+Ç ˜‹W>ª·rh++¶\6¥õ$FžEzQ/éÙãz¬'=QÁ\ê›}ÍÓÒ (‹¦ˆè2¡ â$Hûtë (`Û ÿ‰{$nPH´2°`3`.‡ #}ÚܽG×2ÐCUx©$—D|MÊk劼N°-Oì‰I•Øã.•Ü¥{Ó%u©¤.ÝSÑ’¬Wå­àBtJƼNª¡aéÌ_OO™/yIìÓ\ ¨Í €Š,Œ¯Z5¼ìÆ ¿õµ{üìoî}ejïRˆÎ‹j_ÏÌxcýú‹6Z¶~ˆñ»ŸaáõGë»Ãuò÷@&j‡€Èk·¢$£œÆîHåX2‘˜+¨ví;³žÇJ}+ Ã&˜ëOeAP½‰ÔLð„A©€+©•áˆvåCˆÚ8vï1ñdÇÈä„8Ñx¼´%¶LiB|I|… 4ÁX§ÓAd ï@ä²[ö&ø0´¤L0¾ ó1•­i7ŽÈŠ´ºõ•±õúŠòÓL¤ìä|þð ²eÌ-ϺEÚnÝeç°s´óœ7²7j÷r8U±%0Ê ;”;T÷‹÷Ç*T"tjEÙŠ$ãQê÷ù”wñ>Ÿ0Î*eÈ·Ã÷wõœŠ:_.p…ÙœYóÎìy~hf¡•ý=Èå3à7zØ»åï›lwG·×°¨B\Îl(Û°˜Ae|%Þ-×XÛ¾|]íÕÑþåÛ¸mŠëí78¶åFf]ß²­í¦ö;íw:¶·sûìû¯f_m›X~xùÑåÇ—»]’µZÌYjüË+Ô4º‘­ ,p#gó7¿ 2›-*%˜Lb%2G‚ÌÄ^ï#)˜‘4;"ODž°‘q|ÿþîä(\ð¨¬#Ïšv@àÃó–( äšÂ+xVvŒ.À d¨]{”ÔYÐaÁ–q¬”Íë”x‹2FhF™ã·ÓÝ€Yë\ N9q‡sÈÉ8Ÿe~;‚Uì"ÔÀfd5/8á÷ZÊË ‹žcÓÀï|pÍ£ElZöƒ÷j]z[zGšM;Mk ÛKçò•ìP'î$cÓnCæµ}"|‘ÖG C‚èÁ:#þ8†ˆ† ‚.³Ûâ¸=ÞŸˆŽsq=yn•"ö ó…l"Òiüjiyz¹¼|'̹b9yÕ£Ñf—ë·ÝÕŠ[©-§5#Ù°ÁÖo{ˆýxñﲑ¼gÓÁÀFûžÏgeóöFܘI³,ÓÁÂU‘eX2¥No–¦Ð*¤'¨ŽO2O’1²—,[þÞˆX½{+¸=Jž6Ð,§?z&“ƒÇÄä©PÚ’2 £AÙ@‘¦™l€Ñ(N’X_2Eò<° àûÞ |`€O€wÄ2DÙ÷fäƒÔ /QÝâ“úzN[Ž®i[ZßÎy¼vã@U¦:“Ͱ|S´=Z)‹.‰tz°gìÀkË-’ÐlÜ(¡™ŠFê¨XäAç%;%<ÇÑêÁ]±¥¼d©·Þ »g …™n[«‘™f èø,®ÁƒÏIëA‹çJ¨ÅÞ ;Ê¡—%Cµ6•LN%¯>¹$DœnÇ!øô©ÉIVWŠ£°’XœŽï†XVx´ŸöÕÓÐp«,@¡Ð´EA$Æ™îœ!Z<Ù AâÈké[¸ @Ýþ±(èa$0`ºå\ç²C;¯ï{1©gykH~§î¥çÌ-÷Òžþ_Ïì]wé}'_¸±McÌ +²É<¶.X5'Û±ð–êÂשtýªg÷=V½÷C|Nâ‡=7¿$+x•Ý¥Vðóú‡X¢y‹Q8V¡ÒõŸ7pÑK«jŽÈlÕEþŒ?t3¼ášû—μfDz٧¾WÝI‡gm™—µÙ8`úäרØ€>WÃl›æÞ:`z¦ª6ª)#T;¤ì n{0Ž~MÐ9*<à =18`OÀ§€–P ds± à´Z0(Ð6ÒFÅxñ_ûH-d¾¢f+È”p 2ŸËòzm¯vmš`{&2Á3g eñrÔ–•«A1£·\8“ààM¸îçŸPNk„Ôö$¾ôJ8ˆ^š!¨ˆ„ ŸÖ»³€Ö|WŽ^ዱ,4Jš4ÆÔ”ýª)ËUS¶¬ž¶vѪiû—£®è“Z O`4Ç©ý2‡ùâɈO=I˜xEE]í4צL{:Zd’¸é@‘ ±È»åT\–S×õÜlˆ¢Cu£uÜXÝDÝá:6É㎺¾º~R%×aIéHøÀûgÁŠ„/¶ ¨NøÄ¡@Âgõre(«lÊúrs°«At” V¢Úé«FÕxL ê~õõ›j@à^¥œ*Q|Èü‰>E°ì4º½³Ÿb›D "ööÀºÀ`ÃÁu€Ã}ðÃT’¥z;ÁF>È›A|ˆú¡^ñý’* àO±à ÐÌ$ >ƒ :‰â@€^I;ûÚÚÀÜA2MM¥Œì¬­å»dbîÚÉ3ä£ AÁL†÷•ì!˜¤R…C:Š:†€½ŽâY  óÅRCñÁá‡Îš=}ÿPã!' 7œ£aÜî†w†‡R¸#ÌÈä&Œ³ª*KÓºúR ¾NZEh*W:]Y@ó‚ .á3ZÄœM’/0GëÔšGa(y„‚ZÁlRBT[žðà=Í9’ȆÆ{™V«sêÂ9™‡Žƒ/§¦>;êÀÜçèwŒ:v:Ž;Ž=¡=?¥è@º Ò*È«–ÄTpÃÀÐJv‚ % ^2 Ÿõ“æ3pMÁšª>®e3f”•5Ìø®3ÓThn®t«ŸË×c‹â6r£¡¬lF!0%-É »ºðÊ;Ë%§!ÜOlÅ‹ ­x›bÀm¿4Mé5q3UƒÌ~²‚'öM3”!S=È‘Í%-A·šhM:Ðß ôÈ|N¡2 Ð ™#ð£Ô~Ä'bbµq¨*asÿZóÝ!b¹ß9T"Õ@ýNƒfòÐ^Üç¼'É\7ÖætÉ=@ådGr4¹K¿Ë»3ÉKPJ²"ÔN².e<&5Å|ñ9N2$¾ËìR•9ÝRB+ØÆ±ü!ð‹¦|Ù°™ˆñ«¡¬´Ð°ÈV&ív¬p n©ùà1@oØï•°A‚_ÝÚ)—XI"ƒÍòKÐáiOYò­YuNÈß™P¨–‹ç|¼è¬?ˆ[Ä@ØX²÷Žñ‡Üû(ÄMö½Â¥íÁyS2¾q7ˆŸÞàx ~öéÁ·@Ãm!þ‰šË@ŽéùÖ/‚Ïve¡£õd’@N<ÙÐúÕÎåÝ™€Ëm\pTÚ¾ŸmôvY²¡ Zýٱ١P•NXYz;sëÝÉÀ4 aø}PÄ€öYñí%:ˆì€ø”òY´<ü…L÷Ÿaž0z ¡¿ŸöT}Jy?Ô”ˆdÞ&I]VÜO}WŠgÉ+á€Fc>ãµ"±Tï'«Î(Ö„—§2èÖgñó˜™Ò/ 5ä§üêî´NM—t 4kør‰A†ˆÔqU"GZ­Ýv9‚¯R{.¡@OŽÚ'ìÇí,ìxšØÛØš%©\ŸŸ‘Åö=ºU5v,Û;ì}ö~û¨}'<(h>aA'||,tÚ]x5Âa|›6CRÙ•›‘Õâ-îÓökGµ;µÇµ íÛY¥ÄX(PÂ,Õl©séÛTƒpH²ô×:³s •.½ßáŠÃÏ0(n;Ù´¤ÎK)+ÿx.q!‘ØF#D§A?]Êþvš:Ø{¨ØC­#v#]Zc×B,Ilù”.©‘ „í¤“ô©d¦¶õôS)=Ejäyªµin}®‰J”¦…°¥ŠéZxú=È”$DÈ”€Ì¿d'ª…jÒÌÂ$}=I_OÖÂ’Bh(¦Zº%ÊoË4Þ´ÖC†2ˆ§äíZ°ì“+i£ÖHÛ0Ò6Œ€×Ÿ”ÚÒä(¿XjC*#m@ù=YCÚ ÞZ>0 íH6gªªe!tÒÜÎ.™<“êÂí]뺶t±]Kø¹G¤\Š’ß~D„x ’‡Ä© òwZb%P]U)€Og§AàX‰“`O"üûŒ9In€æ¡u :»–ŽÌ\#…x£DR’ЧIZ—¬m¢¥&ZjZãú”òbIê†yúšJ®4CP2§wkk»a ¾ ø™AækzwážîiÄ‹#t‘\Eè9=a\ ÝR®š§*<¦ƒŸg{–Ÿ 8Sp¦‹Ÿìw9 úÕAüð×ã–=YápÏ_mìØlzˆ ÖþÑw¥„6PžÚ¬Mø2‘5Á… ßÜAc¿%¤ßJ&|ž¡ÛjJøZ!#Ï uÅ5uúºæ(µ‹ä|"®DBdî’¥da"åZµFà9…0·¶NØÕ=ÀàÇi ÷KcN“œl¨MT&ÃuéZÜ_;VËÔ’:Û¢¥Má… ý‹:1C‹F1h‘¸ˆYx}ÀbË.êëîg–í €¬<ŽWÑß'"%ó "1iÒpá =“?`Ö?=AXÄéø²ƒ€¹…¿’4 k ºH(Ö ÃÔGΖ¦!Övëõ×3ÈÈD˜þ_Dj¢Á–~djÁþ 9S vðojÏæ,Õ¸c•©bmõ’ë¬knk›?°éÔ53 滚sÇ–ä.[È0ÖúÖBfa^£”·×äW83m…U.ÊblI2Ÿ¯2DËV­ØØÖÖU]aÃÉ¢·] ;ðH¥œ›§IÚ¨<σºŒì-¯-X—Õ¸Ãa÷Œ.|ÁÝåßð)øqö¿€–U3ghYŽÒ2b$bº2ôªWl!B*I]ÈN()QšÞ=M)‚ÒFUßéˆuêµ›N‰üA¦d™/ä(ÁxòRr⥠yiÞÕ|T©Mô!ŸƒÌq*)A¦Dæ ó/YMZI ¸ý“¬ÊP™)S¥#?Y$„taY6„«Wy)Š#•¢Š¯Hc9¾¥ý‚ó¨-„‚íx‰^ζCˤlD¦)ÙÏ24O;)µo“mL|—’Ò %¥JuŽÚh• ܽà<µËÙKŸôÒ /½é¥%ïÓ ùdþþ$y%‘Èe§ ÆÿSM¸>š°2G(@:בëËõçFsŠ Ë4?¥±?–;œcÆr¸/7”›È±^¥-á3””âDÂ^T&|ú!oÂ*)Å™XYSÚ—™ãA¡ªj:£áPB‘Õv[XUâ1%6€sf‡òM%§$J1ü$‹7\æOt$úý n(1šK°(!ÂÞ]ÂÉU€ò‰¾lI1¦xþÿO169œ,ÏEœ¬Ýƒ°uÝu‘iÐ;{Ç VL‡‘ü¯Z1°ü³UåiEÄ€jÜöÀm—K6½&3»0Ã,W«¹¦EßÙ ÑT´´f@#ö”0qòŶ% ×6-õ;©>lhÇßÙ<ðý‚·×æ\›» w>8ÏE´ È6Ä+ž—ÑNK ©LG}ú TŸÔŠ$\QëwÙ þ$#›I%GãìÑ(F@ê#Þ Ê §UÕoŸ*rŸ<ç"/» L¹8 …8‹¶Gƒ Œ®Ð8H$ûJûø¨ªsß½öžçž÷û•ÌÞ“yg&3ÉLLHL€ QDT¬–`ˆŠ’žÖ§í §ÚV¥5ÔsP[ê!LƒÔšöp¬} Ü{¬·ö×N›Z=Å–Zë­ Éù¯5¤ýõþ3É·Þ{¯5{Ö^ë[ßú¾ÿR©‚F£Ä60ÙdD§TÌF¬ºEÒis‘'ÝÇÜ0ן¬~]¯±ÿF$Ëõîu®{Éçô{-¯´’’kR±Ë ‰¼èú¾ŸW$Ò¥[hÕÍ()È{ÐUä4u{Uƒª!Õ>Õ¤J£:GþŠŠqj%—öì¨æš¤º'kº'{¯ÚpÄì:"©º€Iú<ÕUäT i~†N‚W®ÿçr0½q ¹·¬o.‹b~€¹)ÛóME3©¶GÍ1>Z£š˜Í┹jâ—‰[W‹Ãd•I@€ã2xdΧ†CLjò‚”XePÓ§T—°7¨ØFøÍ=â=æ{ìw»G¼#U:ØF•­¢ôUV[!Âéù#†²]´¼R‘6Ãò˜JC±™%xs§÷|üÎWF_¹çæÝ?ZÓôñ%·qÏÖeÂáÇî?üÉ c?ûôž÷ïj/>¶ë¥¹_ø×w?7HׯïÏ­žC_‹s¾¦Ò×’‹™FlN¬¥<ÕÁõ:|œ,$l vÈL! Îl…ŠÀ6î"PÑ’“…DÊ®2küt[°pŠ H&jnîÓhÙÊfGtåÊôNŒ°5BÚˆ÷/Ôæ 7‡Ñƒí©V#ǹÜü…£´#æDÚ'¡ï¢Y+Š‹[Ñ:ÖolŒt -t`ëÊß)¶Â”Q*¡1ÇaFc ´5´ô—.ZË"B*‚GQm û¥¸H\Âá—_¾2oît&o˜ëZÇq3¾wÓU1áá6ÏÏò?FiP5VúK<ÏúKzÑüZž0i3aÒfbIv$üÚxÈBext6Fà]%Gûƒ¥A«‹[B*{JMvªÉmj¢Žf !µZß]A² ØŒQÙOÀûíÐJ‚žfª,|xýT½NϘO½zÊújy¼»$DÎ…,qªÖ´gÔ|mƒ¶|Ÿ½[M>®þ¤šWGkµA²9¸JQ»Ð¾£`gI³ÖbÉçü:3 êâзЬÇó9Ö[°kSöOb¦ë‡†T¿õäÉþ¢õ$Ó^G£è¸—Ô§}iÞnÏ(†Bzá^gŸqCl¿õ¡ˆZÔBI<9˜Êå5–ü4‘•û1DþÐôCóÉÈÉèÿ ¿y=ý†êð‘·Ò{1ÝŸþDÝîô8çÇ…1ט,0Vµ·n‹m•fèc¾Œ„ýøôDœ ƇâcqAŽ×ÇùøsDærD>RÖ.ÂXMmµÙÞÎEªG4‚Q! µýÒÅ Ùp6òÙôþÈþ´º¿šÙ²¯ O¸F <ˆZŕĨÏÅ'2HJ3‚U‰@8f™:˜è¡Å<_!B-,3©g„õÝtÅ®Àz/ëò ´l§×œÇfŠY ‚Õ„zLôç» õ˜Pü%JŒqc,Ûßrðl¨íä&a¶aÆ Õ+ú-l®ƒ™ú‚ê9v¾`·Ç &­…ߊÝuýÒkeià ?|~äšÛB.)ªzìÆÎuç~QW·ÿ“Í«ò6«Ý(ž{éÁ[WÔ-J$3Ë6=¾û‘ è'Ë>÷W:oØ×ZX·ýaÅìÅœçœÿߦúPl.VưhµbÛV y=¿Ö`dËd£ËAÔt°‰Ì±°ßŒÀ»Œ…Cà|yãÙaÐ¥-n' =qÜ6£Š§.ª÷í“YÚÏl>’ùšÿRL“²xñtÿŒ›rý »BøY¡?lBÚ…2ÀóÅêGlø^p½àžö½éÓNT“½~¨ª÷˜Œ¦?y±^tyãsy}~PÇ8@W}¥µB=ìÉ5Æ&Úh÷+P`ü= Ý·8?â Ó䜒–1yf²Õ“PŒ.¦J¥Ž8{d Ðø«cÒ1ã8í8ëÐ8«Aï¤ÌÀ£ý@D‡@½@Äœ¥È§V@ç[g ¦OdÇØÌÔ.À™ÝAW¯¶¼ ¶ù¬›å¡]M+ª}³`ñ®xíµ|"t…-ëȬ¯ýÇ–á:ORõ¹_zñ_ú®H&nÜ”ØÄßro]ÛÂáC°2˜.â|·(__éWî8“õ`h£kObÙm…#’™;˜îÙò®–ìgýv&'DUYÁòšwÙÖ«=²°D0{£ƒlöjªÓf¨Óâ->J—:‘îÖ)ð³à¤ æX4}/O•%ºT?ý£Å²N[Vût¢A6xÍP±Ã]Ë·4í@mt·‚N†Döƒ¤k^šãišß®ÓÅdÖ÷dÀ9b!ÇÐÚwXïC ¼ÓJ¬÷ÙíñX¥÷1 -*›eZ(R6Ýi€#ÄB& 4‘8¸Èq:CLÆU†©U^.-—Õ~£‡®B=Áh<¬‹“vmP×!¢ÕºiÒ©8DhcR¢È,Dƒ!Ä”ÎÍ8ÂX,Cdˆ%*@´BÍÀîóCÎÖëØçàÇàL:ÚíäJÇC·‹}·¬†~Ij El‡ ÿQUƒ ¼Ã[ZÐ6`“‡5Pe±UYüUÀX«a™Fõ ¨ú9lrhWüH»|¡'Bµ@ÛªôO(Ä›„MÐ,—âæ¹ßÕݹ«sÕötUËrÒÞWLÝÞ]Ø 7FiÏHôâþ±Þ敼vu ̵±·‚>ú6ú¨Ì§ÜGéõœß®aHä6pæ2ˆ~yºo°9w®˜Åœ@*&" ^Q€UqMלLHçthlµ´/Øìž¥à —Y@¦÷9•ú迬®•ýù)¬éϪ·¯×{¯óáX ¾Ö`¹e£«ÉésúÃú1d“í¯ì“ý­ú‚Øj§À‰­þº.}‡Øéíôuù·ê¾¢{DÿUÿ£‰š¯sOéê÷=ÓìoC±ú˜xÌû¬ï9ÿ‰ÀLͽï‰ïy?ô×Mè ­e*7ØÈüTCÙ&Ë>,%Xz<^öÃá²o³1_Q|U–š]`¯ø!õ.ùSê{mã5úV]£Ø˘53¡Ÿøµˆ{½÷û„ûr/ïð:ƒ. 9»h â-¸ÆÙ~Ÿìõùêõ¢6Ú¿?¢×!Ä…RéÀ”9ì`œ8ßg€¬Ô€ µtbމ¯Šjq·J¯7+VE“= ;®{`›»õ¾?51•9=¾ŸÅÞ¨§ßŠ|Ô/嚨÷¬±‰ÓÏ`Á4M^8f­!cö«”¢þ1‹£1D‡Vtñ¶Šþ‹Þ7|X½ïúߦþ^°Ze@?ôy:¾ÕïÿŠq€9|;jÊÖõËØGEÖÒ¼Þ|¾>ŽËð)VœUDGA' Ä–×lÿ™jhSèFªÚæp°%3ƒV…¥$C<€ÒlÜFWÅ“®¿æÑ#”jt†«æN$玻’-'<Éáú9 oZTmÖ[ O·—^ø nÎZõ:6¦›ægÕÏà}I §*ïK,´™ù4Š›9}Ì«S%¢’Æ¢¡½XÌf!;ÈÆ²Aù­$fÐ:.z«¨«c.ÖJxi0¾R×Ó«¸»ùNØÇp#0É5Œ@+ÔP¾{:] eê訉ђÖUìàÅÏûYe”Ñe²tÊaØ¡GuÐb“;ŽE¦-—3™­ú¡Ì[Ñ·Žþ9a¤JŽ&VÔÊd’››«}À{ [3*1VKÇ ±µž'=OzŸŒé Ñ–HK¼‡[IVi»tË"Kã««’hǬc¶ÏGH<ËÁC9xæ(.MÐÒ¡!Л$cIB÷䤂­™äé¤69Xw‰szZ·Û}þ·/ÎBÇv{åíF’ ˜à¼³~°S”èëNy,zæ8Êf±8Ür.í‚ô,6P$8Ýè ,¤ü?ã-P´6d@öKÍ1j`®ÄzèhŒ‚¿ÒÅ UûG{¶TM±_/yN;_òð,Ï—\,vÄu ó-;˜V¬ÃqS+¢è¡•¡¤'aâ4Äâ&2†‰øä¿5zãî6òÌò ¬t¾ãŒHh]rîåä¯çþûiõ¢6Œ(ª`•”¾øòôým3ìüì:]ß!6ËŽ š¶^ø-ßuñYïÊCá•ò8Xø ƘEÂ;¾Ñ½1U‡›e1Òy’z'u'µðtH…ðÀôÌp  xýŽÃöó<¤ªn¡‡id ¿Ix±÷ò¦¢¯²MØ<(bs ê+à°©éþ“–‚¢4ÓP+ž ÎV(&z·óÇ,…¨l¡“þOJj°u^Þ12L,…Ê(bMÄ`·á:ŠÙKH1˜g+³ SÁŠ`y а(§Å@Pâ¢@¿t\Àv9õÀÍÀÃzû¼âpšu®BÒIMç.€i:«Ü…¤b¹ 9J¨ÙCkÑËà\¾æ/ý¿–‹\*Æ2Ø@TÁíóPÁÿ%Fë(ŸSÞˆÓÁɽpPL UÄ ÃÉPØànï^^#Í ‘†µ»g¯Y^˜ë­ƒâá}vÔÕÍý8ˆm˜ù—W} CS•Ç›³ÖÜrË&¿«“·æŽ'ç¦w6‘ˆÓìñôŸTHˆÁe‘ö¹g-!ŸÕ“G/ŽWÇ;çräƒd®7˜À²{Cæâ…OÜû@Gº6ï¶\Ñ7ÁOI™°Ñ ÍŒöIÌ­·¡ÿºÈו¬]§òª&T¦ ó×UÓ*턇˜<#¦†æ^n½¥×àUÙa¹AuµåŒê´E[™ÃDð¸ oV±yðI5éUbÿ Þ¨é°2`Ù† ëyR' ”ÌÁƒÃ?Û³Á—{Ïjmw©€+¢äÔêgÄ AÈ r ‚J0ð* 1š=&Z‹ªû õ&¨. @Â_OxÑr‚¿‚3>å %-̾V¦×DêM  E“?ë)zz  jÌ“X>·çkåidõ»ÛW½;KŠž7Pé Ç ÌJ‘: mDSik¸ûwŸôBf÷O¯ÿ—ANÁÆ~óüiE‘^¨‡ÃLX‹¸ ØDùÙ1wA•pÒàO€Õ«jÝôü¾c°FöºhðÍc.-,xb71*ö!ÔDBÐHfKÈEB+âzÃ…Ÿðƒs¯nlsT À]|”¬ÞÚí±ˆoî7¡Öέ˜‹^x5œ–o¦¿=ö#øñ³û__=`iû^w$pÜ‹ s/.øTÈuãˆëYyšë´¡¹Nn•ûpÇg ­Mïtù§AS U0¹çøop×€2ô|5nR{·Qý=Ϊ¾–«­@8 Z޲y³W£þÞü¯i\5ÌòÃðÇT¿Â™Õßã »öó8Šðó\—Š›ÿþRÇuÀ_‰ë{þÈ„zÚøÂü&øƒÅ€ iFP'®yåLB5·yN¤ñ îk‚q/¼°iãöp3Ä \õ; ÌU;4ÍIíÝ.œ½ÖfØ`xÓø†¹ÙòŒuжÙþ„ó î}ž’÷—þ•UmÕ{¤„n¬ùvøläåxcâÍÚ`ê3é¯dæêkvåMK[Zônáöä¸w8û*jè©xYîZü,¿Tý–S#NŸ­½ò|5ØæV_½¬¯¯7uÍÖÛ· ¯Þr×ÕÛnßø‰Þ5«®¡„Ï|§‘ÿ­¶ŸPðZΆ»VhWXBŹZ. “Í ™å¹&œ#ÞÂ-¢ì×Áuâôòe8?¼ ç‚wãòU°Ðîå®â®æÖpkÑÖu8]½§š_Ç]Ïõ`å¹)¡V*¶»„YnPx‹›~Í© 7 ÂÑŽ !„çAêùá?¦:;sÊ4üT†ù¥D2wœf”üU¹ç…ÿ࿉B½F8SrXÎ/JK–T͋ʩںÜØ ý‚û=ˆ~!œÞ9» (¹óí&$aL¹ Q?ç&A<§?ŠÄr/?Bþ„ïãñÑ˾_2Ùr¸á÷„gñ°$á˜p´’stÊlËqíÃè%„›{tt¤â¶ Or£ qÐaгÀ•@YPM ‡Ð΃¸Þ7 Ú©ð¿ôSWxJ¸Èý’ð9:BÃÿ¬ð óÿ¾ñÇ‘„ÿ5Ä©?Q‰ï‡Oó­¤?‚¸ñ‡+þ—‘@üKˆSÿ‹•øÂ»nGÅ? —‚’µ=ˆ|Tz¡‡ðèBŒƒK„O ·±ŸÃo/ûx»K¡0ûvOy|¹x¤»ñèwãÉíÆ“ÛM•§„] ev•ËÔ »PfÊìB™]x*õÂ0ꦽ®$ƒ<÷a wt$pŸ»t€Æ„»ð“hÕ^áÖRBBg»yª äŠ'„›ð¨á&ˆsãÅô"íˆ7MéÍßBËnae·Lé4uË”¿ºì£ÔÇÛÍÂ&î“ z_›¸¨ÔR ›J‘¬ôœ°š»]Ç)fi”FU£jU}±¿]±^Œ¼l'ë¸6HJm¤eP?¤Ó ô Šz½¢ïÕ«· £Â¸ Ðs+ŠB0 ¨)[®mÍS¶h™¦5¿ÏpÀ0i˜1œ6¨'53šÓš³šóÀŽe÷j5Cš1Í>˜ƒé)¨?h2Œ+64ê Š¡× –´ä@û½Âøš\+h´¤Â3@º,ÜÀ¯1€ÇvÒ9¸bVÐi„ÏÂW#fA9 ÊYjAª©\šÓ  h.lÓàÒT: :Ò€âÈ5#€³H7#!Ð ÄLˆ™3¡ÔiþZh…+ƒzAK;‹z Ü…¼úJþ | Góσ p—æ) Jêã3I2I{!û’Di+¶ç”8À™DUÛÂÛ¢ÛÛªzÂ=ÑžDÏAU1\ŒŃ*lSG³‰ìA•L)!T¯<¼ò…•¯¬T ¬Ü¶rt¥uý™©RªK8ø5Qê-ùü¹Kûbþ0¾ÎÜ ÐãÃ\Tm©øÃp%þi¤>Ô§¹ÐH+žÆõ¸4ŸæÑô š…Î ÄÿE¾€/þÍRk¾§}†ÜÐøMp‹ Zº:ÌÒ'ážeé=più ÚÊo^º þÂÚ¸¨ ÔÜ+Â:LëèáJ !ÐaJØ€¿uÂ:þiü}“ÿ&VL .‰s»1Ùm:k»•7¢˜ÈSÌ}˜¹{™[dnD1¯0½·Âôí¦ûV˜âÙ¼<ÄÜbh7=Ónêi7%ÛM¸›‡ kv\ uÉo™»š¹iÅ2½2ý1dúCÈôÕi{Èô±½'Øâ 's Ô%_bî æÆp"·éEÉ´N2µH¦vyŒ  °«§n¹ê˜wX8ý òf]OJmIišç˜GæKmíÒ4™+µ-ƒw±Ôö¼JmJß"ïc‹Sy¯™•Ú]ä]Ò¥¢ñ?Vü?.îâçáß ÿ ®Dáÿs©íS´ü?áúGœ«ÑÑë¾Æõ²ë'HKÿj庯”Ò7¢Öý¥ôNÔú(亴ô—KéY¤>XJï…÷…Rú6xã¥(mà­¥¶Z©ÝF€ªÉÓ²›°óK[²²R#ŽèânC|YùâÎRš^ÕA+˜&W– ðâ´•ß"a®—U'•ÂìK‚yaƒ*8kt€‹2ßL,¬ñ&®†ùºRøS¸‹æ™è¬ô¿ÛNÐ/Îý‰XJI¿ú¾ßµˆþ’t•Iÿã8}\%é•4Nÿ>&½>!ý[dš\[’fÒÓ:d¼žæÉQéò$Êòä˜t8}³ôt˜å #?õD[´?¼Az$ŠxIúTú[´ÜíøÆ×"»/}…´²í´b0d+mô¨qQj ß!¼hštM’"Ó´)õ¸Ç¡cR-jŒA4ަ¬myŽo‚hlDIkwhoÔ^«½J»X›×Ö©Z[¥uêì:«Î¬P´N€"l“é8“ÊgR”•tj·®Á°M8 [14¼€ÔÅÚHÇãÝÁ¦n7ß½f ™´wsÝ×,™lIuOk篞\h]ïuëò}ˆMòLîšõÓdž&ݘ´Ó³ÉÞûyhE²ëÞÏcï¶{rf×}£<ùÞ|ñª “êð/ç¾³è-Ú¯°–vü g%v\&-ÂZý²·zòKÝkÖO~£ºo2GóÕ}Ý“ËÖÈÀñÛùmÇù!êõ­?Nîá·w^MÓÉ=}—ŠÁ.|ŰP€G‹Mq5´WC¦X±•ìnè¦5GjàÐBß%]´ºÏwY¡›Y!ôñíô^½ÔC1>ÈEؽ"|C(ßÌrùÍŒ±°›YŒ»Y-t$E}i8}ë´DQàH´…eú(;̲“>Ž8ÎEI«‡°zÊ·H”Ë TÊð:”¹ì!þ÷ƒ[–ü܃LmüÙæM[ƒáÎ- ÁÉÏÞy‹wrìFY>²ùg4Cžbƒ7nº…ú·Lþ,¼¥crs¸C>²‘]÷WÙ›höÆpÇnSç5ëlR¶t”6*;Ã;ú¦ž½²û/êÚ{©®+GÿF]£ôfWÒºž`×ýU]Ý4û ZW7­«›Öõ„ò««ûê%¤»wý·„j¤1 ßx¡¾%nëÐìåXòî <§â0mR}“Æð’Iˆ¾7uíuí4 o'Í2#ÙRÉòîY þitÇ3<Ì¡’aP*EkK@…¯½†Ûn—€3¯ƒñpåAk@jî_áþ;èW ?‚Tܧá>ú'ÐMꄺNïÖZcîxœó ¹©ú¦6rSo*ûk6”ýÎÕe¿­=ÍË\©˜Û-`¼ ÷Ü€~ úOÐ µrìæh3ýô sÃÐ^¡"éÔNí 8Ý+EèãÞ1 8JÈ ø¨¢#{¼ˆW>áð(ðƒÀC!–>L/C¸¶ò¡Šÿ ñuo endstream endobj 316 0 obj 27999 endobj 317 0 obj << /Type /FontDescriptor /Ascent 891 /CapHeight 792 /Descent -216 /Flags 32 /FontBBox [-77 -216 936 695] /FontName /NRGYYP+TimesNewRomanPSMT /ItalicAngle 0 /StemV 0 /Leading 42 /MaxWidth 2000 /XHeight 594 /FontFile2 315 0 R >> endobj 318 0 obj [ 250 0 0 0 0 0 0 0 333 333 0 0 250 0 250 278 0 0 0 0 0 0 0 0 0 0 278 0 0 0 0 0 921 722 667 667 722 0 0 722 0 333 0 0 0 889 722 722 556 0 667 556 611 722 722 944 0 0 0 0 0 0 0 0 0 444 500 444 500 444 333 500 500 278 278 0 278 778 500 500 500 0 333 389 278 500 500 722 500 500 444 ] endobj 8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /NRGYYP+TimesNewRomanPSMT /FontDescriptor 317 0 R /Widths 318 0 R /FirstChar 32 /LastChar 122 /Encoding /MacRomanEncoding >> endobj 1 0 obj << /Title (Untitled) /Author (Anthony Brummett) /Creator (soffice) /Producer (Mac OS X 10.5.6 Quartz PDFContext) /CreationDate (D:20090514170054Z00'00') /ModDate (D:20090514170054Z00'00') >> endobj xref 0 319 0000000000 65535 f 0000645667 00000 n 0000000384 00000 n 0000581203 00000 n 0000000022 00000 n 0000000365 00000 n 0000000488 00000 n 0000001500 00000 n 0000645483 00000 n 0000000586 00000 n 0000001480 00000 n 0000002309 00000 n 0000001535 00000 n 0000002289 00000 n 0000002416 00000 n 0000595978 00000 n 0000000000 00000 n 0000583746 00000 n 0000002829 00000 n 0000002541 00000 n 0000002809 00000 n 0000002936 00000 n 0000003086 00000 n 0000040310 00000 n 0000040622 00000 n 0000040332 00000 n 0000040602 00000 n 0000040729 00000 n 0000040879 00000 n 0000112584 00000 n 0000112786 00000 n 0000112606 00000 n 0000112767 00000 n 0000112893 00000 n 0000112984 00000 n 0000215526 00000 n 0000215765 00000 n 0000215549 00000 n 0000215745 00000 n 0000215872 00000 n 0000216344 00000 n 0000215971 00000 n 0000216324 00000 n 0000216451 00000 n 0000616650 00000 n 0000217069 00000 n 0000216551 00000 n 0000217049 00000 n 0000217176 00000 n 0000217918 00000 n 0000581327 00000 n 0000217276 00000 n 0000217898 00000 n 0000218026 00000 n 0000219583 00000 n 0000218126 00000 n 0000219562 00000 n 0000219691 00000 n 0000220048 00000 n 0000219791 00000 n 0000220028 00000 n 0000220156 00000 n 0000220995 00000 n 0000220256 00000 n 0000220975 00000 n 0000221103 00000 n 0000221254 00000 n 0000249280 00000 n 0000250573 00000 n 0000249302 00000 n 0000250552 00000 n 0000250681 00000 n 0000251000 00000 n 0000250781 00000 n 0000250980 00000 n 0000251108 00000 n 0000251505 00000 n 0000251208 00000 n 0000251485 00000 n 0000251613 00000 n 0000252568 00000 n 0000251713 00000 n 0000252548 00000 n 0000252676 00000 n 0000252957 00000 n 0000581453 00000 n 0000252776 00000 n 0000252938 00000 n 0000253065 00000 n 0000253156 00000 n 0000362997 00000 n 0000363859 00000 n 0000363020 00000 n 0000363839 00000 n 0000363967 00000 n 0000364250 00000 n 0000364067 00000 n 0000364231 00000 n 0000364358 00000 n 0000364449 00000 n 0000541255 00000 n 0000541499 00000 n 0000541279 00000 n 0000541478 00000 n 0000541610 00000 n 0000542214 00000 n 0000541711 00000 n 0000542193 00000 n 0000542325 00000 n 0000542981 00000 n 0000542426 00000 n 0000542960 00000 n 0000543092 00000 n 0000544159 00000 n 0000543193 00000 n 0000544138 00000 n 0000544270 00000 n 0000545034 00000 n 0000544371 00000 n 0000545013 00000 n 0000545145 00000 n 0000545956 00000 n 0000581584 00000 n 0000545246 00000 n 0000545935 00000 n 0000546068 00000 n 0000547185 00000 n 0000546169 00000 n 0000547164 00000 n 0000547297 00000 n 0000547692 00000 n 0000547398 00000 n 0000547671 00000 n 0000547804 00000 n 0000548336 00000 n 0000547905 00000 n 0000548315 00000 n 0000548448 00000 n 0000549200 00000 n 0000548549 00000 n 0000549179 00000 n 0000549312 00000 n 0000550222 00000 n 0000549413 00000 n 0000550201 00000 n 0000550334 00000 n 0000551377 00000 n 0000550435 00000 n 0000551356 00000 n 0000551489 00000 n 0000551818 00000 n 0000551590 00000 n 0000551797 00000 n 0000551930 00000 n 0000552342 00000 n 0000581719 00000 n 0000552031 00000 n 0000552321 00000 n 0000552454 00000 n 0000552967 00000 n 0000552555 00000 n 0000552946 00000 n 0000553079 00000 n 0000553618 00000 n 0000553180 00000 n 0000553597 00000 n 0000553730 00000 n 0000554328 00000 n 0000553831 00000 n 0000554307 00000 n 0000554440 00000 n 0000555066 00000 n 0000554541 00000 n 0000555045 00000 n 0000555178 00000 n 0000555884 00000 n 0000555279 00000 n 0000555863 00000 n 0000555996 00000 n 0000556868 00000 n 0000556097 00000 n 0000556847 00000 n 0000556980 00000 n 0000557862 00000 n 0000557081 00000 n 0000557841 00000 n 0000557974 00000 n 0000558801 00000 n 0000581854 00000 n 0000558075 00000 n 0000558780 00000 n 0000558913 00000 n 0000559278 00000 n 0000559014 00000 n 0000559257 00000 n 0000559390 00000 n 0000560019 00000 n 0000559491 00000 n 0000559998 00000 n 0000560131 00000 n 0000561053 00000 n 0000560232 00000 n 0000561032 00000 n 0000561165 00000 n 0000561517 00000 n 0000561266 00000 n 0000561496 00000 n 0000561629 00000 n 0000562299 00000 n 0000561730 00000 n 0000562278 00000 n 0000562411 00000 n 0000563282 00000 n 0000562512 00000 n 0000563261 00000 n 0000563394 00000 n 0000564809 00000 n 0000563495 00000 n 0000564787 00000 n 0000564921 00000 n 0000565243 00000 n 0000581989 00000 n 0000565022 00000 n 0000565222 00000 n 0000565355 00000 n 0000566122 00000 n 0000565456 00000 n 0000566101 00000 n 0000566234 00000 n 0000567246 00000 n 0000566335 00000 n 0000567225 00000 n 0000567358 00000 n 0000567849 00000 n 0000567459 00000 n 0000567828 00000 n 0000567961 00000 n 0000568804 00000 n 0000568062 00000 n 0000568783 00000 n 0000568916 00000 n 0000569962 00000 n 0000569017 00000 n 0000569941 00000 n 0000570074 00000 n 0000571148 00000 n 0000570175 00000 n 0000571127 00000 n 0000571260 00000 n 0000571746 00000 n 0000571361 00000 n 0000571725 00000 n 0000571858 00000 n 0000572317 00000 n 0000582124 00000 n 0000571959 00000 n 0000572296 00000 n 0000572429 00000 n 0000572901 00000 n 0000572530 00000 n 0000572880 00000 n 0000573013 00000 n 0000573157 00000 n 0000573211 00000 n 0000574002 00000 n 0000573266 00000 n 0000573981 00000 n 0000574114 00000 n 0000575226 00000 n 0000574258 00000 n 0000575205 00000 n 0000575338 00000 n 0000576571 00000 n 0000575482 00000 n 0000576550 00000 n 0000576683 00000 n 0000577265 00000 n 0000576827 00000 n 0000577244 00000 n 0000577377 00000 n 0000578108 00000 n 0000577478 00000 n 0000578087 00000 n 0000578220 00000 n 0000578882 00000 n 0000578321 00000 n 0000578861 00000 n 0000578994 00000 n 0000579671 00000 n 0000582259 00000 n 0000579095 00000 n 0000579650 00000 n 0000579783 00000 n 0000580990 00000 n 0000579884 00000 n 0000580968 00000 n 0000581102 00000 n 0000582346 00000 n 0000582478 00000 n 0000582557 00000 n 0000582653 00000 n 0000582706 00000 n 0000583174 00000 n 0000583195 00000 n 0000583415 00000 n 0000583440 00000 n 0000583725 00000 n 0000583914 00000 n 0000595474 00000 n 0000595497 00000 n 0000595744 00000 n 0000596168 00000 n 0000615845 00000 n 0000615868 00000 n 0000616089 00000 n 0000616825 00000 n 0000644917 00000 n 0000644940 00000 n 0000645184 00000 n trailer << /Size 319 /Root 300 0 R /Info 1 0 R /ID [ ] >> startxref 645873 %%EOF ModuleBase.pm100664023532023421 5674212544604516 15345 0ustar00abrummetgsc000000000000UR-0.44/lib/UR# A base class supplying error, warning, status, and debug facilities. package UR::ModuleBase; use Sub::Name; use Sub::Install; BEGIN { use Class::Autouse; # the file above now does this, but just in case: # subsequent uses of this module w/o the special override should just do nothing... $INC{"Class/Autouse_1_99_02.pm"} = 1; $INC{"Class/Autouse_1_99_04.pm"} = 1; no strict; no warnings; # ensure that modules which inherit from this never fall into the # replaced UNIVERSAL::can/isa *can = $Class::Autouse::ORIGINAL_CAN; *isa = $Class::Autouse::ORIGINAL_ISA; } =pod =head1 NAME UR::ModuleBase - Methods common to all UR classes and object instances. =head1 DESCRIPTION This is a base class for packages, classes, and objects which need to manage basic functionality in the UR framework such as inheritance, AUTOLOAD/AUTOSUB methods, error/status/warning/etc messages. UR::ModuleBase is in the @ISA list for UR::Object, but UR::ModuleBase is not a formal UR class. =head1 METHODS =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; # set up module use Carp; use IO::Handle; use UR::Util; =pod =over =item C $class = $obj->class; This returns the class name of a class or an object as a string. It is exactly equivalent to: (ref($self) ? ref($self) : $self) =cut sub class { my $class = shift; $class = ref($class) if ref($class); return $class; } =pod =item C $sub_ref = $obj->super_can('func'); This method determines if any of the super classes of the C<$obj> object can perform the method C. If any one of them can, reference to the subroutine that would be called (determined using a depth-first search of the C<@ISA> array) is returned. If none of the super classes provide a method named C, C is returned. =cut sub super_can { my $class = shift; foreach my $parent_class ( $class->parent_classes ) { my $code = $parent_class->can(@_); return $code if $code; } return; } =pod =item C @classes = $obj->inheritance; This method returns a depth-first list of all the classes (packages) that the class that C<$obj> was blessed into inherits from. This order is the same order as is searched when searching for inherited methods to execute. If the class has no super classes, an empty list is returned. The C class is not returned unless explicitly put into the C<@ISA> array by the class or one of its super classes. =cut sub inheritance { my $self = $_[0]; my $class = ref($self) || $self; return unless $class; no strict; my @parent_classes = @{$class . '::ISA'}; my @ordered_inheritance; foreach my $parent_class (@parent_classes) { push @ordered_inheritance, $parent_class, ($parent_class eq 'UR' ? () : inheritance($parent_class) ); } return @ordered_inheritance; } =pod =item C MyClass->parent_classes; This returns the immediate parent class, or parent classes in the case of multiple inheritance. In no case does it follow the inheritance hierarchy as ->inheritance() does. =cut sub parent_classes { my $self = $_[0]; my $class = ref($self) || $self; no strict 'refs'; my @parent_classes = @{$class . '::ISA'}; return (wantarray ? @parent_classes : $parent_classes[0]); } =pod =item C MyModule->base_dir; This returns the base directory for a given module, in which the modules's supplemental data will be stored, such as config files and glade files, data caches, etc. It uses %INC. =cut sub base_dir { my $self = shift; my $class = ref($self) || $self; $class =~ s/\:\:/\//g; my $dir = $INC{$class . '.pm'} || $INC{$class . '.pl'}; die "Failed to find module $class in \%INC: " . Data::Dumper(%INC) unless ($dir); $dir =~ s/\.p[lm]\s*$//; return $dir; } =pod =item methods Undocumented. =cut sub methods { my $self = shift; my @methods; my %methods; my ($class, $possible_method, $possible_method_full, $r, $r1, $r2); no strict; no warnings; for $class (reverse($self, $self->inheritance())) { print "$class\n"; for $possible_method (sort grep { not /^_/ } keys %{$class . "::"}) { $possible_method_full = $class . "::" . $possible_method; $r1 = $class->can($possible_method); next unless $r1; # not implemented $r2 = $class->super_can($possible_method); next if $r2 eq $r1; # just inherited { push @methods, $possible_method_full; push @{ $methods{$possible_method} }, $class; } } } print Dumper(\%methods); return @methods; } =pod =item C return MyClass->context_return(@return_values); Attempts to return either an array or scalar based on the calling context. Will die if called in scalar context and @return_values has more than 1 element. =cut sub context_return { my $class = shift; return unless defined wantarray; return @_ if wantarray; if (@_ > 1) { my @caller = caller(1); Carp::croak("Method $caller[3] on $class called in scalar context, but " . scalar(@_) . " items need to be returned"); } return $_[0]; } =pod =back =head1 C This package impliments AUTOLOAD so that derived classes can use AUTOSUB instead of AUTOLOAD. When a class or object has a method called which is not found in the final class or any derived classes, perl checks up the tree for AUTOLOAD. We impliment AUTOLOAD at the top of the tree, and then check each class in the tree in order for an AUTOSUB method. Where a class implements AUTOSUB, it will receive a function name as its first parameter, and it is expected to return either a subroutine reference, or undef. If undef is returned then the inheritance tree search will continue. If a subroutine reference is returned it will be executed immediately with the @_ passed into AUTOLOAD. Typically, AUTOSUB will be used to generate a subroutine reference, and will then associate the subref with the function name to avoid repeated calls to AUTOLOAD and AUTOSUB. Why not use AUTOLOAD directly in place of AUTOSUB? On an object with a complex inheritance tree, AUTOLOAD is only found once, after which, there is no way to indicate that the given AUTOLOAD has failed and that the inheritance tree trek should continue for other AUTOLOADS which might impliment the given method. Example: package MyClass; our @ISA = ('UR'); ##- use UR; sub AUTOSUB { my $sub_name = shift; if ($sub_name eq 'foo') { *MyClass::foo = sub { print "Calling MyClass::foo()\n" }; return \&MyClass::foo; } elsif ($sub_name eq 'bar') { *MyClass::bar = sub { print "Calling MyClass::bar()\n" }; return \&MyClass::bar; } else { return; } } package MySubClass; our @ISA = ('MyClass'); sub AUTOSUB { my $sub_name = shift; if ($sub_name eq 'baz') { *MyClass::baz = sub { print "Calling MyClass::baz()\n" }; return \&MyClass::baz; } else { return; } } package main; my $obj = bless({},'MySubClass'); $obj->foo; $obj->bar; $obj->baz; =cut our $AUTOLOAD; sub AUTOLOAD { my $self = $_[0]; # The debugger can't see $AUTOLOAD. This is just here for debugging. my $autoload = $AUTOLOAD; $autoload =~ /(.*)::([^\:]+)$/; my $package = $1; my $function = $2; return if $function eq 'DESTROY'; unless ($package) { Carp::confess("Failed to determine package name from autoload string $autoload"); } # switch these to use Class::AutoCAN / CAN? no strict; no warnings; my @classes = grep {$_} ($self, inheritance($self) ); for my $class (@classes) { if (my $AUTOSUB = $class->can("AUTOSUB")) # FIXME The above causes hard-to-read error messages if $class isn't really a class or object ref # The 2 lines below should fix the problem, but instead make other more impoartant things not work #my $AUTOSUB = eval { $class->can('AUTOSUB') }; #if ($AUTOSUB) { { if (my $subref = $AUTOSUB->($function,@_)) { goto $subref; } } } if ($autoload and $autoload !~ /::DESTROY$/) { my $subref = \&Carp::confess; @_ = ("Can't locate object method \"$function\" via package \"$package\" (perhaps you forgot to load \"$package\"?)"); goto $subref; } } =pod =head1 MESSAGING UR::ModuleBase implements several methods for sending and storing error, warning and status messages to the user. # common usage sub foo { my $self = shift; ... if ($problem) { $self->error_message("Something went wrong..."); return; } return 1; } unless ($obj->foo) { print LOG $obj->error_message(); } =head2 Messaging Methods =over 4 =item message_types @types = UR::ModuleBase->message_types; UR::ModuleBase->message_types(@more_types); With no arguments, this method returns all the types of messages that this class handles. With arguments, it adds a new type to the list. Standard message types are error, status, warning, debug and usage. Note that the addition of new types is not fully supported/implemented yet. =back =cut my $create_subs_for_message_type; # filled in lower down my @message_types = qw(error status warning debug usage); sub message_types { my $self = shift; if (@_) { foreach my $msg_type ( @_ ) { if (! $self->can("${msg_type}_message")) { # This is a new one $create_subs_for_message_type->($self, $msg_type); push @message_types, $msg_type; } } } else { return grep { $self->can($_ . '_message') } @message_types; } } # Most defaults are false my %default_messaging_settings; $default_messaging_settings{dump_error_messages} = 1; $default_messaging_settings{dump_warning_messages} = 1; $default_messaging_settings{dump_status_messages} = 1; # # Implement error_mesage/warning_message/status_message in a way # which handles object-specific callbacks. # # Build a set of methods for getting/setting/printing error/warning/status messages # $class->dump_error_messages() Turn on/off printing the messages to STDERR # error and warnings default to on, status messages default to off # $class->queue_error_messages() Turn on/off queueing of messages # defaults to off # $class->error_message("blah"): set an error message # $class->error_message() return the last message # $class->error_messages() return all the messages that have been queued up # $class->error_messages_arrayref() return the reference to the underlying # list messages get queued to. This is the method for truncating the list # or altering already queued messages # $class->error_messages_callback() Specify a callback for when error # messages are set. The callback runs before printing or queueing, so # you can alter @_ and change the message that gets printed or queued # And then the same thing for status and warning messages =pod For each message type, several methods are created for sending and retrieving messages, registering a callback to run when messages are sent, controlling whether the messages are printed on the terminal, and whether the messages are queued up. For example, for the "error" message type, these methods are created: =over 4 =item error_message $obj->error_message("Something went wrong..."); $obj->error_message($format, @list); $msg = $obj->error_message(); When called with one or more arguments, it sends an error message to the object. The error_message_callback will be run, if one is registered, and the message will be printed to the terminal. When given a single argument, it will be passed through unmodified. When given multiple arguments, error_message will assume the first is a format string and the remainder are parameters to sprintf. When called with no arguments, the last message sent will be returned. If the message is C then no message is printed or queued, and the next time error_message is run as an accessor, it will return undef. =item dump_error_messages $obj->dump_error_messages(0); $flag = $obj->dump_error_messages(); Get or set the flag which controls whether messages sent via C is printed to the terminal. This flag defaults to true for warning and error messages, and false for others. =item queue_error_messages $obj->queue_error_messages(0); $flag = $obj->queue_error_messages(); Get or set the flag which control whether messages send via C are saved into a list. If true, every message sent is saved and can be retrieved with L or L. This flag defaults to false for all message types. =item error_messages_callback $obj->error_messages_callback($subref); $subref = $obj->error_messages_callback(); Get or set the callback run whenever an error_message is sent. This callback is run with two arguments: The object or class error_message() was called on, and a string containing the message. This callback is run before the message is printed to the terminal or queued into its list. The callback can modify the message (by writing to $_[1]) and affect the message that is printed or queued. If $_[1] is set to C, then no message is printed or queued, and the last recorded message is set to undef as when calling error_message with undef as the argument. =item error_messages @list = $obj->error_messages(); If the queue_error_messages flag is on, then this method returns the entire list of queued messages. =item error_messages_arrayref $listref = $obj->error_messages_arrayref(); If the queue_error_messages flag is on, then this method returns a reference to the actual list where messages get queued. This list can be manipulated to add or remove items. =item error_message_source %source_info = $obj->error_message_source Returns a hash of information about the most recent call to error_message. The key "error_message" contains the message. The keys error_package, error_file, error_line and error_subroutine contain info about the location in the code where error_message() was called. =item error_package =item error_file =item error_line =item error_subroutine These methods return the same data as $obj->error_message_source(). =back =cut our $stderr = \*STDERR; our $stdout = \*STDOUT; my %message_settings; # This sub creates the settings mutator subs for each message type # For example, when passed in 'error', it creates the subs error_messages_callback, # queue_error_messages, dump_error_messages, etc $create_subs_for_message_type = sub { my($self, $type) = @_; my $class = ref($self) ? $self->class : $self; my $save_setting = sub { my($self, $name, $val) = @_; if (ref $self) { $message_settings{ $self->class . '::' . $name . '_by_id' }->{$self->id} = $val; } else { $message_settings{ $self->class . '::' . $name } = $val; } }; my $get_setting = sub { my($self, $name) = @_; if (ref $self) { return exists($message_settings{ $self->class . '::' . $name . '_by_id' }) ? $message_settings{ $self->class . '::' . $name . '_by_id' }->{$self->id} : undef; } else { return $message_settings{ $self->class . '::' . $name }; } }; my $make_mutator = sub { my $name = shift; return sub { my $self = shift; if (@_) { # setting the value $save_setting->($self, $name, @_); } else { # getting the value my $val = $get_setting->($self, $name); if (defined $val) { return $val; } elsif (ref $self) { # called on an object and no value set, try the class return $self->class->$name(); } else { # called on a class name my @super = $self->inheritance(); foreach my $super ( @super ) { if (my $super_sub = $super->can($name)) { return $super_sub->($super); } } # None of the parent classes implement it, or there aren't # any parent classes return $default_messaging_settings{$name}; } } }; }; foreach my $base ( qw( %s_messages_callback queue_%s_messages %s_package %s_file %s_line %s_subroutine ) ) { my $method = sprintf($base, $type); my $full_name = $class . '::' . $method; my $method_subref = Sub::Name::subname $full_name => $make_mutator->($method); Sub::Install::install_sub({ code => $method_subref, into => $class, as => $method, }); } my $should_dump_messages = "dump_${type}_messages"; my $dump_mutator = $make_mutator->($should_dump_messages); my @dump_env_vars = map { $_ . uc($should_dump_messages) } ('UR_', 'UR_COMMAND_'); my $should_dump_messages_subref = Sub::Name::subname $class . '::' . $should_dump_messages => sub { my $self = shift; if (@_) { return $dump_mutator->($self, @_); } foreach my $varname ( @dump_env_vars ) { return $ENV{$varname} if (defined $ENV{$varname}); } return $dump_mutator->($self); }; Sub::Install::install_sub({ code => $should_dump_messages_subref, into => $class, as => $should_dump_messages, }); my $messages_arrayref = "${type}_messages_arrayref"; my $message_arrayref_sub = Sub::Name::subname "${class}::${messages_arrayref}" => sub { my $self = shift; my $a = $get_setting->($self, $messages_arrayref); if (! defined $a) { $save_setting->($self, $messages_arrayref, $a = []); } return $a; }; Sub::Install::install_sub({ code => $message_arrayref_sub, into => $class, as => $messages_arrayref, }); my $array_subname = "${type}_messages"; my $array_subref = Sub::Name::subname "${class}::${array_subname}" => sub { my $self = shift; my $a = $get_setting->($self, $messages_arrayref); return $a ? @$a : (); }; Sub::Install::install_sub({ code => $array_subref, into => $class, as => $array_subname, }); my $messageinfo_subname = "${type}_message_source"; my @messageinfo_keys = map { $type . $_ } qw( _message _package _file _line _subroutine ); my $messageinfo_subref = Sub::Name::subname "${class}::${messageinfo_subname}" => sub { my $self = shift; return map { $_ => $self->$_ } @messageinfo_keys; }; Sub::Install::install_sub({ code => $messageinfo_subref, into => $class, as => $messageinfo_subname, }); # usage messages go to STDOUT, others to STDERR my $default_fh = $type eq 'usage' ? \$stdout : \$stderr; my $should_queue_messages = "queue_${type}_messages"; my $check_callback = "${type}_messages_callback"; my $message_text_prefix = ($type eq 'status' or $type eq 'usage') ? '' : uc($type) . ': '; my $message_package = "${type}_package"; my $message_file = "${type}_file"; my $message_line = "${type}_line"; my $message_subroutine = "${type}_subroutine"; my $logger_subname = "${type}_message"; my $logger_subref = Sub::Name::subname "${class}::${logger_subname}" => sub { my $self = shift; if (@_) { my $msg = shift; # if given multiple arguments, assume it's a format string if(@_) { $msg = _carp_sprintf($msg, @_); } defined($msg) && chomp($msg); # old-style callback registered with error_messages_callback if (my $code = $self->$check_callback()) { if (ref $code) { $code->($self, $msg); } else { $self->$code($msg); } } # New-style callback registered as an observer # Some non-UR classes inherit from UR::ModuleBase, and can't __signal if ($UR::initialized && $self->can('__signal_observers__')) { $self->__signal_observers__($logger_subname, $msg); } $save_setting->($self, $logger_subname, $msg); # If the callback set $msg to undef with "$_[1] = undef", then they didn't want the message # processed further if (defined $msg) { if (my $fh = $self->$should_dump_messages()) { $fh = $$default_fh unless (ref $fh); $fh->print($message_text_prefix . $msg . "\n"); } if ($self->$should_queue_messages()) { my $a = $self->$messages_arrayref(); push @$a, $msg; } my ($package, $file, $line, $subroutine) = caller; $self->$message_package($package); $self->$message_file($file); $self->$message_line($line); $self->$message_subroutine($subroutine); } } return $get_setting->($self, $logger_subname); }; Sub::Install::install_sub({ code => $logger_subref, into => $class, as => $logger_subname, }); # "Register" the message type as a valid signal. $UR::Object::Type::STANDARD_VALID_SIGNALS{$logger_subname} = 1; }; sub _carp_sprintf { my $format = shift; my @list = @_; # warnings weren't very helpful because they wouldn't tell you who passed # in the "bad" format string my $formatted_string; my $warn_msg; { local $SIG{__WARN__} = sub { my $msg = $_[0]; my ($filename, $line) = (caller)[1, 2]; my $short_msg = ($msg =~ /(.*) at $filename line $line./)[0]; $warn_msg = ($short_msg || $msg); }; $formatted_string = sprintf($format, @list); } if ($warn_msg) { Carp::carp($warn_msg); } return $formatted_string; } # at init time, make messaging subs for the initial message types $create_subs_for_message_type->(__PACKAGE__, $_) foreach @message_types; sub _current_call_stack { my @stack = reverse split /\n/, Carp::longmess("\t"); # Get rid of the final line from carp, showing the line number # above from which we called it. pop @stack; # Get rid any other function calls which are inside of this # package besides the first one. This allows wrappers to # get_message to look at just the external call stack. # (i.e. AUTOSUB above, set_message/get_message which called this, # and AUTOLOAD in UniversalParent) pop(@stack) while ($stack[-1] =~ /^\s*(UR::ModuleBase|UR)::/ && $stack[-2] && $stack[-2] =~ /^\s*(UR::ModuleBase|UR)::/); return \@stack; } 1; __END__ =pod =head1 SEE ALSO UR(3) =cut # $Header$ ModuleBuild.pm100664023532023421 417612544604516 15504 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::ModuleBuild; use strict; use warnings; use base 'Module::Build'; sub ACTION_clean { # FIXME: is this safe? use File::Path qw/rmtree/; rmtree "./_build"; rmtree "./blib"; unlink "./Build"; unlink "./MYMETA.yml"; } our $ns = 'UR'; our $cmd_class = 'UR::Namespace::Command'; sub ACTION_ur_docs { # We want to use UR to autodocument our code. This is done # with module introspection and requires some namespace hackery # to work. ./Build doc comes after ./Build and copies the root # namespace module into ./blib to fake a Genome namespace so this will work. use File::Copy qw/copy/; $ENV{ANSI_COLORS_DISABLED} = 1; eval { my $oldpwd = $ENV{PWD}; unshift @INC, "$ENV{PWD}/blib/lib"; my ($namespace_src_dr) = grep { -s "$_/$ns.pm" } @INC; unless ($namespace_src_dr) { die "Failed to find $ns.pm in \@INC.\n"; } chdir "$ENV{PWD}/blib/lib/$ns" || die "Can't find $ns/"; unless (-e "../$ns.pm") { copy "$namespace_src_dr/$ns.pm", "../$ns.pm" || die "Can't find $ns.pm"; } eval "use $ns"; $cmd_class->class(); UR::Namespace::Command::Update::Pod->execute( base_commands => [ $cmd_class ], ); # We need to move back for perl install to find ./lib chdir $oldpwd; }; die "failed to extract pod: $!: $@" if ($@); } sub ACTION_docs { my $self = shift; $self->depends_on('ur_docs'); $self->depends_on('code'); $self->depends_on('manpages', 'html'); } 1; __END__ =pod =head1 NAME UR::ModuleBuild - a Module::Build subclass with UR extensions =head1 VERSION This document describes UR::ModuleBuild version 0.44. =head1 SYNOPOSIS In your Build.PL: use UR::ModuleBuild; my $build = UR::ModuleBuild->new( module_name => 'MyApp', license => 'perl', dist_version => '0.01', dist_abstract => 'my app rocks because I get to focus on the problem, not the crud', build_requires => { 'UR' => '0.32', }, requires => { 'Text::CSV_XS' => '', 'Statistics::Distributions' => '', }, ); $build->create_build_script; ModuleConfig.pm100664023532023421 1660012544604516 15665 0ustar00abrummetgsc000000000000UR-0.44/lib/UR# Manage dynamic configuration of modules. package UR::ModuleConfig; =pod =head1 NAME UR::ModuleConfig - manage dynamic configuration of modules. =head1 SYNOPSIS package MyModule; use base qw(UR::ModuleConfig); MyModule->config(%conf); $val = MyModule->config('key'); %conf = MyModule->config; =head1 DESCRIPTION This module manages the configuration for modules. Configurations can be read from files or set dynamically. Modules wishing to use the configuration methods should inherit from the module. =cut # set up package require 5.006_000; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; use base qw(UR::ModuleBase); use IO::File; =pod =head2 METHODS The methods deal with managing configuration. =cut # hash containing all configuration information our %config; # create a combined configuration hash from inheritance tree sub _inherit_config { my $self = shift; my $class = ref($self) || $self; my %cfg; # get all packages inherited from my @inheritance = $self->inheritance; # reverse loop through inheritance tree and construct config foreach my $cls (reverse(@inheritance)) { if (exists($config{$cls})) { # add hash, overriding previous values %cfg = (%cfg, %{$config{$cls}}); } } # now add the current class config if (exists($config{$class})) { %cfg = (%cfg, %{$config{$class}}); } # now add the object config if (ref($self)) { # add the objects config if (exists($config{"$class;$self"})) { %cfg = (%cfg, %{$config{"$class;$self"}}); } } return %cfg; } =pod =over 4 =item config MyModule->config(%config); $val = MyModule->config('key'); %conf = MyModule->config; my $obj = MyModule->new; $obj->config(%config); This method can be called three ways, as either a class or object method. The first method takes a hash as its argument and sets the configuration parameters given in the hash. The second method takes a single argument which should be one of the keys of the hash that set the config parameters and returns the value of that config hash key. The final method takes no arguments and returns the entire configuration hash. When called as an object method, the config for both the object and all classes in its inheritance hierarchy are referenced, with the object config taking precedence over class methods and class methods closer to the object (first in the @ISA array) taking precedence over those further away (later in the @ISA array). When called as a class method, the same procedure is used, except no object configuration is referenced. Do not use configuration keys that begin with an underscore (C<_>). These are reserved for internal use. =back =cut sub config { my $self = shift; my $class = ref($self) || $self; # handle both object and class configuration my $target; if (ref($self)) { # object config $target = "$class;$self"; } else { # class config $target = $self; } # lay claim to the modules configuration $config{$target}{_Manager} = __PACKAGE__; # see if values are being set if (@_ > 1) { # set values in config hash, overriding any current values my (%opts) = @_; %{$config{$target}} = (%{$config{$target}}, %opts); return 1; } # else they want one key or the whole hash # store config for object and inheritance tree my %cfg = $self->_inherit_config; # see how we were called if (@_ == 1) { # return value of key my ($key) = @_; # make sure hash key exists my $val; if (exists($cfg{$key})) { $self->debug_message("config key $key exists"); $val = $cfg{$key}; } else { $self->error_message("config key $key does not exist"); return; } return $val; } # else return the entire config hash return %cfg; } =pod =over 4 =item check_config $obj->check_config($key); This method checks to see if a value is set. Unlike config, it does not issue a warning if the key is not set. If the key is not set, C is returned. If the key has been set, the value of the key is returned (which may be C). =back =cut sub check_config { my $self = shift; my ($key) = @_; # get config for inheritance tree my %cfg = $self->_inherit_config; if (exists($cfg{$key})) { $self->debug_message("configuration key $key set: $cfg{$key}"); return $cfg{$key}; } # else $self->debug_message("configuration key $key not set"); return; } =pod =over 4 =item default_config $class->default_config(%defaults); This method allows the developer to set configuration values, only if they are not already set. =back =cut sub default_config { my $self = shift; my (%opts) = @_; # get config for inheritance tree my %cfg = $self->_inherit_config; # loop through arguments while (my ($k, $v) = each(%opts)) { # see is config value is already set if (exists($cfg{$k})) { $self->debug_message("config $k already set"); next; } $self->debug_message("setting default for $k"); # set config key $self->config($k => $v); } return 1; } =pod =over 4 =item config_file $rv = $class->config_file(path => $path); $rv = $class->config_file(handle => $fh); This method reads in the given file and expects key-value pairs, one per line. The key and value should be separated by an equal sign, C<=>, with optional surrounding space. It currently only handles single value values. The method returns true upon success, C on failure. =back =cut sub config_file { my $self = shift; my (%opts) = @_; my $fh; if ($opts{path}) { # make sure file is ok if (-f $opts{path}) { $self->debug_message("config file exists: $opts{path}"); } else { $self->error_message("config file does not exist: $opts{path}"); return; } if (-r $opts{path}) { $self->debug_message("config file is readable: $opts{path}"); } else { $self->error_message("config file is not readable: $opts{path}"); return; } # open file $fh = IO::File->new("<$opts{path}"); if (defined($fh)) { $self->debug_message("opened config file for reading: $opts{path}"); } else { $self->error_message("failed to open config file for reading: " . $opts{path}); return; } } elsif ($opts{handle}) { $fh = $opts{handle}; } else { $self->error_message("no config file input specified"); return; } # read through file my %fconfig; while (defined(my $line = $fh->getline)) { # clean up chomp($line); $line =~ s/\#.*//; $line =~ s/^\s*//; $line =~ s/\s*$//; next unless $line =~ m/\S/; # parse my ($k, $v) = split(m/\s*=\s*/, $line, 2); $fconfig{$k} = $v; } $fh->close; # update config return $self->config(%fconfig); } 1; #$Header$ ModuleLoader.pm100664023532023421 744712544604516 15657 0ustar00abrummetgsc000000000000UR-0.44/lib/UR package UR::ModuleLoader; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; Class::Autouse->autouse(\&dynamically_load_class); Class::Autouse->sugar(\&define_class); our @CARP_NOT = qw(Class::Autouse UR::Namespace); my %loading; sub define_class { my ($class,$func,@params) = @_; return unless $UR::initialized; return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get"); #return if $loading{$class}; #$loading{$class} = 1; # Handle the special case of defining a new class # This lets us have the effect of a UNIVERSAL::class method, w/o mucking with UNIVERSAL if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") { my @class_params; if (@params == 2 and ref($params[1]) eq 'HASH') { @class_params = %{ $params[1] }; } elsif (@params == 2 and ref($params[1]) eq 'ARRAY') { @class_params = @{ $params[1] }; } else { @class_params = @params[1..$#params]; } my $class_meta = UR::Object::Type->define(class_name => $class, @class_params); unless ($class_meta) { die "error defining class $class!"; } return sub { $class }; } else { return; } } sub dynamically_load_class { my ($class,$func,@params) = @_; # Don't even try to load unless we're done boostrapping somewhat. return unless $UR::initialized; return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get"); # Some modules (Class::DBI, recently) call UNIVERSAL::can directly with things which don't even resemble # class names. Skip doing any work on anything which isn't at least a two-part class name. # We refuse explicitly to handle top-level namespaces below anyway, and this will keep us from # slowing down other modules just to fail late. my ($namespace) = ($class =~ /^(.*?)::/); return unless $namespace; if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") { # a "class" statement caught by the above define_class call return; } unless ($namespace->isa("UR::Namespace")) { return; } # TODO: this isn't safe against exceptions # Instead, localize %loading with a copy of the previous %loading plus one class return if $loading{$class}; $loading{$class} = 1; unless ($namespace->should_dynamically_load_class($class)) { delete $loading{$class}; return; } # Attempt to get a class object, loading it as necessary (probably). # TODO: this is a non-standard accessor my $meta = $namespace->get_member_class($class); unless ($meta) { delete $loading{$class}; return; } # Handle the case in which the class is not "generated". # These are generated by default when used, so this is a corner case. unless ($meta->generated()) { # we have a new class # attempt to auto-generate it unless ($meta->generate) { Carp::confess("failed to auto-generate $class"); } } delete $loading{$class}; # Return a descriptive error message for the caller. my $fref; if (defined $func) { $fref = $class->can($func); unless ($fref) { Carp::confess("$class was auto-generated successfully but cannot find method $func"); } return $fref; } return 1; }; 1; =pod =head1 NAME UR::ModuleLoader - UR hooks into Class::Autouse =head1 DESCRIPTION UR uses Class::Autouse to handle automagic loading for modules. As long as some part of an application "use"s a Namespace module, the autoloader will handle loading modules under that namespace when they are needed. =head1 SEE ALSO UR, UR::Namespace =cut Namespace.pm100664023532023421 1705212544604516 15210 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Namespace; use strict; use warnings; use File::Find; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Namespace', is => ['UR::Singleton'], is_abstract => 1, has => [ domain => { is => 'Text', is_optional => 1, len => undef, doc => "DNS domain name associated with the namespace in question", }, allow_sloppy_primitives => { is => 'Boolean', default_value => 1, doc => 'when true, unrecognized data types will function as UR::Value::SloppyPrimitive' }, method_resolution_order => { is => 'Text', value => ($^V lt v5.9.5 ? 'dfs' : 'c3'), valid_values => ($^V lt v5.9.5 ? ['dfs'] : ['dfs', 'c3']), doc => 'Method Resolution Order to use for this namespace. C3 is only supported in Perl >= 5.9.5.', }, ], doc => 'The class for a singleton module representing a UR namespace.', ); sub get_member_class { my $self = shift; return UR::Object::Type->get(@_); } # FIXME These should change to using the namespace metadata DB when # that's in place, rather than trolling through the directory tree sub get_material_classes { my $self = shift->_singleton_object; my @classes; if (my $cached = $self->{material_classes}) { @classes = map { UR::Object::Type->get($_) } @$cached; } else { my @names; for my $class_name ($self->_get_class_names_under_namespace()) { my $class = eval { UR::Object::Type->get($class_name) }; next unless $class; push @classes, $class; push @names, $class_name; } $self->{material_classes} = \@names; } return @classes; } # Subclasses can override this method to tell the dynamic module loader # whether it should go ahead and load the given module name or not. # The default behavior is to go ahead and try for them all sub should_dynamically_load_class { # my($self,$class_name) = @_; return 1; } sub get_material_class_names { return map {$_->class_name} $_[0]->get_material_classes(); } # Returns data source objects for all the data sources of the namespace sub get_data_sources { my $class = shift; if ($class eq 'UR' or (ref($class) and $class->id eq 'UR')) { return 'UR::DataSource::Meta'; # UR only has 1 "real" data source, the other stuff in that dir are base classes } else { my %found; my $namespace_name = $class->class; foreach my $inc ( @main::INC ) { my $path = join('/', $inc,$namespace_name,'DataSource'); if (-d $path) { foreach ( glob($path . '/*.pm') ) { my($module_name) = m/DataSource\/([^\/]+)\.pm$/; my $ds_class_name = $namespace_name . '::DataSource::' . $module_name; $found{$ds_class_name} = 1; } } } my @data_sources = map { $_->get() } keys(%found); return @data_sources; } } sub get_base_contexts { return shift->_get_class_names_under_namespace("Context"); } sub get_vocabulary { my $class = shift->_singleton_class_name; return $class . "::Vocabulary"; } sub get_base_directory_name { my $class = shift->_singleton_class_name; my $dir = $class->__meta__->module_path; $dir =~ s/\.pm$//; return $dir; } sub get_deleted_module_directory_name { my $self = shift; my $meta = $self->__meta__; my $path = $meta->module_path; $path =~ s/.pm$//g; $path .= "/.deleted"; return $path; } # FIXME This is misnamed... # It really returns all the package names under the specified directory # (assumming the packages defined in the found files are named like the # pathname of the file), not just those that implement classes sub _get_class_names_under_namespace { my $class = shift->_singleton_class_name; my $subdir = shift; Carp::confess if ref($class); my $dir = $class->get_base_directory_name; my $namespace_dir; if (defined($subdir) and length($subdir)) { $namespace_dir = join("/",$dir, $subdir); } else { $namespace_dir = $dir; } my $namespace = $class; my %class_names; my $preprocess = sub { if ($File::Find::dir =~ m/\/t$/) { return (); } elsif (-e ($File::Find::dir . "/UR_IGNORE")) { return (); } else { return @_; } }; my $wanted = sub { return if -d $File::Find::name; # not interested in directories return if $File::Find::name =~ /\/\.deleted\//; # .deleted directories are created by ur update classes return if -e $File::Find::name . '/UR_IGNORE'; # ignore a whole directory? return unless $File::Find::name =~ m/\.pm$/; # must be a perl module return unless $File::Find::name =~ m/.*($namespace\/.*)\.pm/; my $try_class = $1; return if $try_class =~ m([^\w/]); # Skip names that make for illegal package names. Must be word chars or a / $try_class =~ s/\//::/g; $class_names{$try_class} = 1 if $try_class; }; my @dirs_to_search = @INC; my $path_to_check = $namespace; $path_to_check .= "/$subdir" if $subdir; @dirs_to_search = map($_ . '/' . $path_to_check, @dirs_to_search); # only look in places with namespace_name as a subdir unshift(@dirs_to_search, $namespace_dir) if (-d $namespace_dir); @dirs_to_search = grep { $_ =~ m/\/$path_to_check/ and -d $_ } @dirs_to_search; return unless @dirs_to_search; find({ wanted => $wanted, preprocess => $preprocess }, @dirs_to_search); return sort keys %class_names; } 1; =pod =head1 NAME UR::Namespace - Manage collections of packages and classes =head1 SYNOPSIS In a file called MyApp.pm: use UR; UR::Object::Type->define( class_name => 'MyApp', is => 'UR::Namespace', ); Other programs, as well as modules in the MyApp subdirectory can now put use MyApp; in their code, and they will have access to all the classes and data under the MyApp tree. =head1 DESCRIPTION A UR namespace is the top-level object that represents your data's class structure in the most general way. After use-ing a namespace module, the program gets access to the module autoloader, which will automatically use modules on your behalf if you attempt to interact with their packages in a UR-y way, such as calling get(). Most programs will not interact with the Namespace, except to C its package. =head1 Methods =over 4 =item get_material_classes my @class_metas = $namespace->get_material_classes(); Return a list of L class metadata object that exist in the given Namespace. Note that this uses File::Find to find C<*.pm> files under the Namespace directory and calls Cget($name)> for each package name to get the autoloader to use the package. It's likely to be pretty slow. =item get_material_class_names my @class_names = $namespace->get_material_class_names() Return just the names of the classes produced by C. =item get_data_sources my @data_sources = $namespace->get_data_sources() Return the data source objects it finds defined under the DataSource subdirectory of the namespace. =item get_base_directory_name my $path = $namespace->get_base_directory_name() Returns the directory path where the Namespace module was loaded from. =back =head1 SEE ALSO L, L, L =cut Command.pm100664023532023421 51712544604516 16524 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespacepackage UR::Namespace::Command; # This is the module behind the "ur" executable. use strict; use warnings; use UR; use UR::Namespace::Command::Base; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', doc => 'tools to create and maintain a ur class tree' ); 1; Command.pm.opts100664023532023421 4022412544604516 17547 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace$UR::Namespace::Command::OPTS_SPEC = [ '>define', [ '>class', [ 'extends=s', 'files', 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'names=s@', 'files', 'help!', undef ], '>datasource', [ '>file', [ 'server=s', 'files', 'singleton!', undef, 'debug!', undef, 'dsid=s', 'files', 'namespace-name=s', 'files', 'verbose!', undef, 'dsname=s', 'files', 'help!', undef ], '>mysql', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'debug!', undef, 'dsid=s', 'files', 'namespace-name=s', 'files', 'server=s', 'files', 'verbose!', undef, 'dsname=s', 'files', 'help!', undef ], '>oracle', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'debug!', undef, 'dsid=s', 'files', 'namespace-name=s', 'files', 'server=s', 'files', 'verbose!', undef, 'dsname=s', 'files', 'help!', undef ], '>pg', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'debug!', undef, 'dsid=s', 'files', 'namespace-name=s', 'files', 'server=s', 'files', 'verbose!', undef, 'dsname=s', 'files', 'help!', undef ], '>sqlite', [ 'nosingleton!', undef, 'debug!', undef, 'dsid=s', 'files', 'namespace-name=s', 'files', 'server=s', 'files', 'verbose!', undef, 'dsname=s', 'files', 'help!', undef ], 'help!', undef ], '>db', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'uri=s', 'files', 'name=s', 'files', 'help!', undef ], '>namespace', [ 'debug!', undef, 'nsname=s', 'files', 'help!', undef ], 'help!', undef ], '>init', [ 'debug!', undef, 'namespace=s', 'files', 'db=s', 'files', 'help!', undef ], '>list', [ '>classes', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>modules', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>objects', [ 'style=s', [ 'text', 'csv', 'tsv', 'pretty', 'html', 'xml', 'newtext' ], 'csv-delimiter=s', 'files', 'noheaders!', undef, 'subject-class-name=s', undef, 'show=s', 'files', 'order-by=s', 'files', 'filter=s', 'files', 'help!', undef ], 'help!', undef ], '>old', [ '>diff-rewrite', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'help!', undef ], '>diff-update', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'help!', undef ], '>export-dbic-classes', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'bare-args=s@', 'files', 'classes-or-modules=s@', 'files', 'help!', undef ], '>info', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'subject=s@', 'files', 'help!', undef ], '>redescribe', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], 'help!', undef ], '>runs-on-modules-in-tree', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>show', [ '>subclasses', [ 'color!', undef, 'maximum-depth=s', undef, 'recalculate!', undef, 'flat!', undef, 'superclass=s', 'files', 'help!', undef ], '>schema', [ 'complete!', undef, 'class-names=s@', 'files', 'help!', undef ], '>properties', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], 'help!', undef ], '>sys', [ '>class-browser', [ 'debug!', undef, 'generate-cache!', undef, 'host=s', 'files', 'namespace-name=s', 'files', 'port=s', undef, 'timeout=s', undef, 'use-cache!', undef, 'verbose!', undef, 'help!', undef ], 'help!', undef ], '>test', [ '>callcount', [ '>list', [ 'file=s', 'files', 'show=s', 'files', 'style=s', [ 'text', 'csv', 'tsv', 'pretty', 'html', 'xml', 'newtext' ], 'csv-delimiter=s', 'files', 'noheaders!', undef, 'order-by=s', 'files', 'filter=s', 'files', 'help!', undef ], 'help!', undef ], '>compile', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>eval', [ 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'bare-args=s@', 'files', 'help!', undef ], '>run', [ 'color!', undef, 'junit!', undef, 'list!', undef, 'lsf!', undef, 'recurse!', undef, 'callcount!', undef, 'cover=s', undef, 'cover-cvs-changes!', undef, 'cover-git-changes!', undef, 'cover-svk-changes!', undef, 'cover-svn-changes!', undef, 'coverage!', undef, 'debug!', undef, 'inc=s@', 'files', 'jobs=s', undef, 'long!', undef, 'lsf-params=s', 'files', 'namespace-name=s', 'files', 'noisy!', undef, 'perl-opts=s', 'files', 'run-as-lsf-helper=s', 'files', 'script-opts=s', 'files', 'time=s', 'files', 'verbose!', undef, 'bare-args=s@', 'files', 'help!', undef ], '>track-object-release', [ 'file=s', 'files', 'debug!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'help!', undef ], '>use', [ 'debug!', undef, 'exec=s', 'files', 'namespace-name=s', 'files', 'summarize-externals!', undef, 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>window', [ 'debug!', undef, 'src=s@', 'files', 'help!', undef ], 'help!', undef ], '>update', [ '>class-diagram', [ 'file=s', 'files', 'data-source=s', 'files', 'debug!', undef, 'depth=s', undef, 'include-ur-object!', undef, 'namespace-name=s', 'files', 'show-attributes!', undef, 'show-methods!', undef, 'verbose!', undef, 'initial-name=s@', 'files', 'help!', undef ], '>classes-from-db', [ 'class-name=s', undef, 'data-source=s', undef, 'debug!', undef, 'force-check-all-tables!', undef, 'force-rewrite-all-classes!', undef, 'namespace-name=s', 'files', 'table-name=s', undef, 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>doc', [ 'restrict-to-input-path!', undef, 'output-format=s', [ 'pod', 'html' ], 'generate-index!', undef, 'suppress-errors!', undef, 'exclude-sections=s@', 'files', 'input-path=s', undef, 'output-path=s', 'files', 'executable-name=s', 'files', 'class-name=s', 'files', 'targets=s@', 'files', 'help!', undef ], '>pod', [ 'input-path=s', undef, 'output-path=s', 'files', 'executable-name=s', 'files', 'class-name=s', 'files', 'targets=s@', 'files', 'help!', undef ], '>rename-class', [ 'debug!', undef, 'force!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>rewrite-class-header', [ 'debug!', undef, 'force!', undef, 'namespace-name=s', 'files', 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>schema-diagram', [ 'file=s', 'files', 'data-source=s', 'files', 'debug!', undef, 'depth=s', undef, 'namespace-name=s', 'files', 'show-columns!', undef, 'verbose!', undef, 'initial-name=s@', 'files', 'help!', undef ], '>tab-completion-spec', [ 'debug!', undef, 'namespace-name=s', 'files', 'output=s', 'files', 'verbose!', undef, 'classname=s', 'files', 'help!', undef ], 'help!', undef ], 'help!', undef ]; Base.pm100664023532023421 3204112544604516 17433 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Base; use strict; use warnings; use UR; use Cwd; use Carp; use File::Find; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command::V1', is_abstract => 1, has => [ namespace_name => { type => 'String', is_optional => 1, doc => 'Name of the Namespace to work in. Auto-detected if within a Namespace directory' }, lib_path => { type => "FilesystemPath", doc => "The directory in which the namespace module resides. Auto-detected normally.", is_constant => 1, calculate_from => ['namespace_name'], calculate => q( # the namespace module should have gotten loaded in create() my $namespace_module = $namespace_name; $namespace_module =~ s#::#/#g; my $namespace_path = Cwd::abs_path($INC{$namespace_module . ".pm"}); unless ($namespace_path) { Carp::croak("Namespace module $namespace_name has not been loaded yet"); } $namespace_path =~ s/\/[^\/]+.pm$//; return $namespace_path; ), }, working_subdir => { type => "FilesystemPath", doc => 'The current working directory relative to lib_path', calculate => q( my $lib_path = $self->lib_path; return UR::Util::path_relative_to($lib_path, Cwd::abs_path(Cwd::getcwd)); ), }, namespace_path => { type => 'FilesystemPath', doc => "The directory under which all the namespace's modules reside", is_constant => 1, calculate_from => ['namespace_name'], calculate => q( my $lib_path = $self->lib_path; return $lib_path . '/' . $namespace_name; ), }, verbose => { type => "Boolean", is_optional => 1, doc => "Causes the command to show more detailed output." }, ], doc => 'a command which operates on classes/modules in a UR namespace directory' ); sub create { my $class = shift; my ($rule,%extra) = $class->define_boolexpr(@_); my($namespace_name, $lib_path); if ($rule->specifies_value_for('namespace_name')) { $namespace_name = $rule->value_for('namespace_name'); $lib_path = $class->resolve_lib_path_for_namespace_name($namespace_name); } else { ($namespace_name,$lib_path) = $class->resolve_namespace_name_from_cwd(); unless ($namespace_name) { $class->error_message("Could not determine namespace name."); $class->error_message("Run this command from within a namespace subdirectory or use the --namespace-name command line option"); return; } $rule = $rule->add_filter(namespace_name => $namespace_name); } # Use the namespace. $class->status_message("Loading namespace module $namespace_name") if ($rule->value_for('verbose')); # Ensure the right modules are visible to the command. # Make the module accessible. # We'd like to "use lib" this directory, but any other -I/use-lib # requests should still come ahead of it. This requires a little munging. # Find the first thing in the compiled_inc list that exists my $compiled = ''; for my $path ( UR::Util::compiled_inc() ) { next unless -d $path; $compiled = Cwd::abs_path($path); last if defined $compiled; } my $perl5lib = ''; foreach my $path ( split(':', $ENV{'PERL5LIB'}) ) { next unless -d $path; $perl5lib = Cwd::abs_path($path); last if defined $perl5lib; } my $i; for ($i = 0; $i < @INC; $i++) { # Find the index in @INC that's the first thing in # compiled-in module paths # # since abs_path returns undef for non-existant dirs, # skip the comparison if either is undef my $inc = Cwd::abs_path($INC[$i]); next unless defined $inc; last if ($inc eq $compiled or $inc eq $perl5lib); } splice(@INC, $i, 0, $lib_path); eval "use $namespace_name"; if ($@) { $class->error_message("Error using namespace module '$namespace_name': $@"); return; } my $self = $class->SUPER::create($rule); return unless $self; unless (eval { UR::Namespace->get($namespace_name) }) { $self->error_message("Namespace '$namespace_name' was not found"); return; } if ($namespace_name->can("_set_context_for_schema_updates")) { $namespace_name->_set_context_for_schema_updates(); } return $self; } sub command_name { my $class = shift; return "ur" if $class eq __PACKAGE__; my $name = $class->SUPER::command_name; $name =~ s/^u-r namespace/ur/; return $name; } sub help_detail { return shift->help_brief } # Return a list of module pathnames relative to lib_path sub _modules_in_tree { my $self = shift; my @modules; my $lib_path = $self->lib_path; my $namespace_path = $self->namespace_path; my $wanted_closure = sub { if (-f $_ and m/\.pm$/) { push @modules, UR::Util::path_relative_to($lib_path, $_); } }; unless (@_) { File::Find::find({ no_chdir => 1, wanted => $wanted_closure, }, $namespace_path); } else { # this method takes either module paths or class names as params # normalize to module paths NAME: for (my $i = 0; $i < @_; $i++) { my $name = $_[$i]; if ($name =~ m/::/) { # It's a class name my @name_parts = split(/::/, $name); unless ($self->namespace_name eq $name_parts[0]) { $self->warning_message("Skipping class name $name: Not in namespace ".$self->namespace_name); next NAME; } $name = join('/', @name_parts) . ".pm"; } # First, check the pathname relative to the cwd CHECK_LIB_PATH: foreach my $check_name ( $name, $lib_path.'/'.$name, $namespace_path.'/'.$name) { if (-e $check_name) { if (-f $check_name and $check_name =~ m/\.pm$/) { push @modules, UR::Util::path_relative_to($lib_path, $check_name); next NAME; # found it, don't check the other $check_name } elsif (-d $check_name) { File::Find::find({ no_chdir => 1, wanted => $wanted_closure, }, $check_name); } elsif (-e $check_name) { $self->warning_message("Ignoring non-module $check_name"); next CHECK_LIB_PATH; } } } } } return @modules; } sub _class_names_in_tree { my $self = shift; my @modules = $self->_modules_in_tree(@_); my $lib_path = $self->lib_path; my @class_names; for my $module (@modules) { my $class = $module; $class =~ s/^$lib_path\///; $class =~ s/\//::/g; $class =~ s/\.pm$//; # Paths can have invalid package names so are therefore packages in # another "namespace" and should not be included. next unless UR::Util::is_valid_class_name($class); push @class_names, $class; } return @class_names; } sub _class_objects_in_tree { my $self = shift; my @class_names = $self->_class_names_in_tree(@_); my @class_objects; for my $class_name (sort { uc($a) cmp uc($b) } @class_names) { unless(UR::Object::Type->use_module_with_namespace_constraints($class_name)) { #if ($@) { print STDERR "Failed to use class $class_name!\n"; print STDERR $@,"\n"; next; } my $c = UR::Object::Type->is_loaded(class_name => $class_name); unless ($c) { #print STDERR "Failed to find class object for class $class_name\n"; next; } push @class_objects, $c; #print $class_name,"\n"; } return @class_objects; } # Tries to guess what namespace you are in from your current working # directory. When called in list context, it also returns the directroy # name the namespace module was found in sub resolve_namespace_name_from_cwd { my $class = shift; my $cwd = shift; $cwd ||= Cwd::cwd(); my @lib = grep { length($_) } split(/\//,$cwd); SUBDIR: while (@lib) { my $namespace_name = pop @lib; my $lib_path = "/" . join("/",@lib); my $namespace_module_path = $lib_path . '/' . $namespace_name . '.pm'; if (-e $namespace_module_path) { if ($class->_is_file_the_namespace_module($namespace_name, $namespace_module_path)) { if (wantarray) { return ($namespace_name, $lib_path); } else { return $namespace_name; } } } } return; } # Returns true if the given file is the namespace module we're looking for. # The only certain way is to go ahead and load it, but this should be good # enough for ligitimate use cases. sub _is_file_the_namespace_module { my($class,$namespace_name,$namespace_module_path) = @_; my $fh = IO::File->new($namespace_module_path); return unless $fh; while (my $line = $fh->getline) { if ($line =~ m/package\s+$namespace_name\s*;/) { # At this point $namespace_name should be a plain word with no ':'s # and if the file sets the package to a single word with no colons, # it's pretty likely that it's a namespace module. return 1; } } return; } # Return the pathname that the specified namespace module can be found sub resolve_lib_path_for_namespace_name { my($class,$namespace_name,$cwd) = @_; unless ($namespace_name) { Carp::croak('namespace name is a required argument for UR::Util::resolve_lib_path_for_namespace_name()'); } # first, see if we're in a namespace dir my($resolved_ns_name, $lib_path ) = $class->resolve_namespace_name_from_cwd($cwd); return $lib_path if (defined($resolved_ns_name) and $resolved_ns_name eq $namespace_name); foreach $lib_path ( @main::INC ) { my $expected_namespace_module = $lib_path . '/' . $namespace_name . '.pm'; $expected_namespace_module =~ s/::/\//g; # swap :: for / if ( $class->_is_file_the_namespace_module($namespace_name, $expected_namespace_module)) { return $lib_path; } } return; } 1; =pod =head1 NAME UR::Namespace::Command - Top-level Command module for the UR namespace commands =head1 DESCRIPTION This class is the parent class for all the namespace-manipluation command modules, and the root for command handling behind the 'ur' command-line script. There are several sub-commands for manipluating a namespace's metadata. =over 4 =item browser Start a lightweight web server for viewing class and schema information =item commit Update data source schemas based on the current class structure =item define Define metadata instances such as classes, data sources or namespaces =item describe Get detailed information about a class =item diff Show a diff for various kinds of other ur commands. =item info Show brief information about class or schema metadata =item list List various types of things =item redescribe Outputs class description(s) formatted to the latest standard =item rename Rename logical schema elements. =item rewrite Rewrites class descriptions headers to normalize manual changes. =item test Sub-commands related to testing =item update Update metadata based on external data sources =back Some of these commands have sub-commands of their own. You can get more detailed information by typing 'ur --help' at the command line. =head1 SEE ALSO Command, UR, UR::Namespace =cut Define.pm100664023532023421 55712544604516 17722 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Define; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => "define namespaces, data sources and classes", ); sub sub_command_sort_position { 2 } sub shell_args_description { "[namespace|...]"; } 1; Class.pm100664023532023421 427712544604516 21012 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Class; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Namespace::Command::Define::Class { is => 'UR::Namespace::Command::Base', has => [ names => { is_optional => 1, is_many => 1, shell_args_position => 1 }, extends => { doc => "The base class. Defaults to UR::Object.", default_value => 'UR::Object' }, ], doc => 'Add one or more classes to the current namespace' }; sub sub_command_sort_position { 3 } sub help_synopsis { return <<'EOS' $ cd Acme $ ur define class Animal Vegetable Mineral A Acme::Animal A Acme::Vegetable A Acme::Mineral $ ur define class Dog Cat Bird --extends Animal A Acme::Dog A Acme::Cat A Acme::Bird EOS } sub execute { my $self = shift; my @class_names = $self->names; unless (@class_names) { $self->error_message("No class name(s) provided!"); return; } my $namespace = $self->namespace_name; my $is = $self->extends || 'UR::Object'; my $parent_class_meta = UR::Object::Type->get($is); unless ($parent_class_meta) { unless ($self->extends =~ /^${namespace}::/) { $parent_class_meta = UR::Object::Type->get($namespace . '::' . $is); if ($parent_class_meta) { $is = $namespace . '::' . $is; } } unless ($parent_class_meta) { $self->error_message("Failed to find base class $is!"); return; } } for my $class_name (@class_names) { unless ($class_name =~ /^${namespace}::/) { $class_name = $namespace . '::' . $class_name; } my $new_class = UR::Object::Type->create( class_name => $class_name, is => $is, ); unless ($new_class) { $self->error_message("Failed to create class $class_name!: " . UR::Object::Type->error_message ); return; } print "A $class_name\n"; $new_class->rewrite_module_header or die "Failed to write class $class_name!: " . $new_class->error_message; } return 1; } 1; Datasource.pm100664023532023421 344112544604516 22027 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define # The diff command delegates to sub-commands under the adjoining directory. package UR::Namespace::Command::Define::Datasource; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has_optional => [ dsid => { is => 'Text', doc => "The full class name to give this data source.", }, dsname => { is => 'Text', shell_args_position => 1, doc => "The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'.", }, ], doc => 'add a data source to the current namespace' ); sub _is_hidden_in_docs { 1 } sub sub_command_sort_position { 2 } sub data_source_module_pathname { my $self = shift; my $ns_path = $self->namespace_path; my $dsid = $self->dsid; my @ds_parts = split(/::/, $dsid); shift @ds_parts; # Get rid of the namespace name my $filename = pop @ds_parts; $filename .= '.pm'; my $path = join('/', $ns_path, @ds_parts, $filename); return $path; } # Overriding these so one can be calculated from the other sub dsid { my $self = shift; my $dsid = $self->__dsid; unless ($dsid) { my $dsname = $self->__dsname; my $namespace = $self->namespace_name; $dsid = $namespace . '::DataSource::' . $dsname; $self->__dsid($dsid); } return $dsid; } sub dsname { my $self = shift; my $dsname = $self->__dsname; unless ($dsname) { my $dsid = $self->__dsid; # assumme the name is the last portion of the class name $dsname = (split(/::/,$dsid))[-1]; $self->__dsname($dsname); } return $dsname; } 1; File.pm100664023532023421 173512544604516 22712 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::File; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Define::Datasource', has => [ server => { is => 'String', doc => '"server" attribute for this data source, such as a database name', }, singleton => { is => 'Boolean', default_value => 1, doc => 'by default all data sources are singletons, but this can be turned off' }, ], doc => 'Add a file-based data source (not yet implemented)' ); sub help_description { "Define a UR datasource connected to a file"; } sub execute { my $self = shift; $self->warning_message("This command is not yet implemented. See the documentation for UR::DataSource::File for more information about creating file-based data sources"); return; } 1; Mysql.pm100664023532023421 63112544604516 23112 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Mysql; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", ); sub help_brief { "Add a MySQL data source to the current namespace." } sub _data_source_sub_class_name { 'UR::DataSource::MySQL' } 1; Oracle.pm100664023532023421 62112544604516 23211 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Oracle; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", doc => "Add an Oracle data source to the current namespace." ); sub _data_source_sub_class_name { 'UR::DataSource::Oracle' } 1; Pg.pm100664023532023421 63012544604516 22352 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Pg; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", ); sub help_brief { "Add a PostgreSQL data source to the current namespace." } sub _data_source_sub_class_name { 'UR::DataSource::Pg' } 1; Rdbms.pm100664023532023421 1002612544604516 23113 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Rdbms; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Define::Datasource', has => [ server => { is => 'String', doc => '"server" attribute for this data source, such as a database name', is_optional => 1, }, nosingleton => { is => 'Boolean', doc => 'Created data source should not inherit from UR::Singleton (defalt is that it will)', default_value => 0, }, ], is_abstract => 1, ); sub help_description { "Define a UR datasource connected to a relational database through UR::DataSource::RDBMS and DBI"; } sub execute { my $self = shift; my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } unless ($self->__dsname || $self->__dsid) { $self->error_message("Either --dsname or --dsid is required"); return; } # Force an autoload of the namespace module #my $ret = above::use_package($namespace); eval "use $namespace"; if ($@) { $self->error_message("Can't load namespace $namespace: $@"); return; } unless (defined $self->server) { $self->server($self->dsname); } my $ds_id = $self->dsid; my $c = eval { UR::DataSource->get($ds_id) || $ds_id->get() }; if ($c) { $self->error_message("A data source named $ds_id already exists\n"); return; } my $src = $self->_resolve_module_header($ds_id,$namespace); my($class_definition,$parent_classes) = $self->_resolve_class_definition_source(); $src .= $class_definition; my $module_body = $self->_resolve_module_body(); $src .= "\n$module_body\n1;\n"; my $module_path = $self->data_source_module_pathname(); my $fh = IO::File->new($module_path, O_WRONLY | O_CREAT | O_EXCL); unless ($fh) { $self->error_message("Can't open $module_path for writing: $!"); return; } $fh->print($src); $fh->close(); $self->status_message("A $ds_id (" . join(',', @$parent_classes) . ")\n"); $self->_post_module_written(); if ($self->_try_connect()) { return 1; } else { return; } } sub _resolve_module_header { my($self,$ds_id, $namespace) = @_; return "package $ds_id;\n\nuse strict;\nuse warnings;\n\nuse $namespace;\n\n"; } # Subclasses can override this to have something happen after the module # is written, but before we try connecting to the DS sub _post_module_written { return 1; } # Subclasses must override this to indicate what abstract DS class they should # inherit from sub _data_source_sub_class_name { my $self = shift; my $class = ref($self); die "Class $class didn't implement _data_source_sub_class_name"; } sub _resolve_class_definition_source { my $self = shift; my $ds_id = $self->dsid; my $parent_ds_class = $self->_data_source_sub_class_name(); my $src = "class $ds_id {\n"; my @parent_classes = ( $parent_ds_class ); if (! $self->nosingleton) { push @parent_classes, 'UR::Singleton'; } $src .= sprintf(" is => [ '%s' ],\n", join("', '", @parent_classes)); $src .= "};\n"; return($src,\@parent_classes); } sub _resolve_module_body { my $self = shift; my $server = $self->server; my $src = "sub server { '$server' }\n"; return $src; } sub _try_connect { my $self = shift; $self->status_message(" ...connecting..."); my $ds_id = $self->dsid; my $dbh = $ds_id->get_default_handle(); if ($dbh) { $self->status_message(" ....ok\n"); return 1; } else { $self->error_message(" ERROR: " . $ds_id->error_message); return; } } 1; RdbmsWithAuth.pm100664023532023421 174412544604516 24560 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::RdbmsWithAuth; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::Rdbms", has => [ login => { is => 'String', doc => 'User to log in with', }, auth => { is => 'String', doc => 'Password to log in with', }, owner => { is => 'String', doc => 'Owner/schema to connect to', }, ], is_abstract => 1, ); sub _resolve_module_body { my $self = shift; my $src = $self->SUPER::_resolve_module_body(@_); my $login = $self->login; $src .= "sub login { '$login' }\n"; my $auth = $self->auth; $src .= "sub auth { '$auth' }\n"; my $owner = $self->owner; $src .= "sub owner { '$owner' }\n"; return $src; } 1; Sqlite.pm100664023532023421 275212544604516 23274 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Sqlite; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => [ 'UR::Namespace::Command::Define::Datasource::Rdbms' ], ); sub help_brief { "Add a SQLite data source to the current namespace." } sub help_synopsis { return <super_can('server'); if (@_) { # unusual case, setting the server return $super_server($self,@_); } my $server = $super_server->($self); unless ($server) { $server = $self->data_source_module_pathname(); $server =~ s/\.pm$/.sqlite3/; $super_server->($self,$server); } return $server; } sub _post_module_written { my $self = shift; # Create a new, empty DB if it dosen't exist yet my $pathname = $self->server; $pathname =~ s/\.pm$/.sqlite3/; IO::File->new($pathname, O_WRONLY | O_CREAT) unless (-f $pathname); $self->status_message("A $pathname (empty database schame)"); return 1; } 1; Db.pm100664023532023421 1613712544604516 20310 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Db; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; # required to import symbols used below UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has_input => [ uri => { is => 'Text', shell_args_position => 1, doc => 'a DBI connect string like dbi:mysql:someserver or user/passwd@dbi:Oracle:someserver~defaultns' }, name => { is => 'Text', shell_args_position => 2, default_value => 'Db1', doc => "the name for this data source (used for class naming)", }, ], has_output_optional => [ _class_name=> { is => 'Text', calculate_from => ['name'], calculate => q| my $namespace = $self->namespace_name; my $dsid = $namespace . '::DataSource::' . $name; return $dsid |, doc => "The full class name to give this data source.", }, _ds => { is_transient => 1, }, ], doc => 'add a data source to the current namespace' ); sub sub_command_sort_position { 2 } sub help_synopsis { return <<'EOS' ur define db dbi:SQLite:/some/file.db Db1 ur define db me@dbi:mysql:myserver MainDb ur define db me@dbi:Oracle:someserver ProdDb ur define db me@dbi:Oracle:someserver~schemaname BigDb ur define db me@dbi:Pg:prod Db1 ur define db me@dbi:Pg:dev Testing::Db1 # alternate for "Testing" (arbitrary) context ur define db me@dbi:Pg:stage Staging::Db1 # alternate for "Staging" (arbitrary) context EOS } sub data_source_module_pathname { my $self = shift; my $class_name = shift; my $ns_path = $self->namespace_path; my @ds_parts = split(/::/, $class_name); shift @ds_parts; # Get rid of the namespace name my $filename = pop @ds_parts; $filename .= '.pm'; my $path = join('/', $ns_path, @ds_parts, $filename); return $path; } sub execute { my $self = shift; my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } my $uri = $self->uri; my ($protocol,$driver,$login,$server,$owner) = ($uri =~ /^([^\:\W]+):(.*?):(.*@|)(.*?)(~.*|)$/); unless ($protocol) { $self->error_message("error parsing URI $uri\n" . 'expected dbi:$driver:$user@$server with optional trailing ~$namespace'); return; } unless ($protocol eq 'dbi') { $self->error_message("currently only the 'dbi' protocol is supported with this command. Other data sources must be hand-written."); return; } $login =~ s/\@$// if defined $login; $owner =~ s/^~// if defined $owner; $self->status_message("protocol: $protocol"); $self->status_message("driver: $driver"); $self->status_message("server: $server"); my $password; if (defined $login) { if ($login =~ /\//) { ($login,$password) = split('/',$login); } $self->status_message("login: $login") if defined $login; $self->status_message("password: $password") if defined $password; } $self->status_message("owner: $owner") if defined $owner; # Force an autoload of the namespace module eval "use $namespace"; if ($@) { $self->error_message("Can't load namespace $namespace: $@"); return; } my $class_name = $self->namespace_name . '::DataSource::' . $self->name; $self->_class_name($class_name); my $c = eval { UR::DataSource->get($class_name) || $class_name->get() }; if ($c) { $self->error_message("A data source named $class_name already exists\n"); return; } my $src = "package $class_name;\nuse strict;\nuse warnings;\nuse $namespace;\n\n"; $src .= "class $class_name {\n"; my $parent_ds_class = 'UR::DataSource::' . $driver; #$self->_data_source_sub_class_name(); $driver =~ s/mysql/MySQL/g; my @parent_classes = ( $parent_ds_class ); push @parent_classes, 'UR::Singleton'; $src .= sprintf(" is => [ '%s' ],\n", join("', '", @parent_classes)); $src .= "};\n"; my $module_body = $self->_resolve_module_body($class_name,$namespace,$driver,$server,$login); $src .= "\n$module_body\n1;\n"; my $module_path = $self->data_source_module_pathname($class_name); my $fh = IO::File->new($module_path, O_WRONLY | O_CREAT | O_EXCL); unless ($fh) { $self->error_message("Can't open $module_path for writing: $!"); return; } $fh->print($src); $fh->close(); $self->status_message("A $class_name (" . join(',', @parent_classes) . ")\n"); #TODO: call a method on the datasource to init the new file my $method = '_post_module_written_' . lc($driver); $self->$method($module_path,$server); unless (UR::Object::Type->use_module_with_namespace_constraints($class_name)) { #if ($@) { $self->error_message("Error in module $class_name!?: $@"); return; } my $ds = $class_name->get(); unless ($ds) { $self->error_message("Failed to get data source for $class_name!"); return; } $self->_ds($ds); if ($self->_try_connect()) { return 1; } else { return; } } sub _resolve_module_body { my ($self,$class_name,$namespace,$driver,$server,$login,$owner) = @_; $owner ||= $login; my $src = <new($server, O_WRONLY | O_CREAT) unless (-f $server); $self->status_message("A $server (empty database schema)"); $pathname =~ s/\.pm$/.sqlite3/; unless ($pathname eq $server) { symlink ($server, $pathname) or die "no symline $pathname for $server! $!"; } return 1; } sub _post_module_written_pg { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_oracle { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_mysql { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_file { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_filemux { my ($self, $pathname, $server) = @_; return 1; } sub _try_connect { my $self = shift; $self->status_message(" ...connecting..."); my $ds = $self->_ds; my $dbh = $ds->get_default_handle(); if ($dbh) { $self->status_message(" ....ok\n"); return 1; } else { $self->error_message(" ERROR: " . $ds->error_message); return; } } 1; Namespace.pm100664023532023421 521012544604516 21625 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Namespace; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => "Command", has => [ nsname => { shell_args_position => 1, doc => 'The name of the namespace, and first "word" in all class names', }, ], doc => 'create a new namespace tree and top-level module', ); sub help_brief { "Used to define a new Namespace as part of starting a new project." } sub sub_command_sort_position { 1 } our $module_template=<nsname; if (-e $name . ".pm") { $self->error_message("Module ${name}.pm already exists!"); return; } eval "package $name;"; if ($@) { $self->error_message("Invalid package name $name: $@"); return; } # Step 1 - Make a new Namespace my $namespace = UR::Object::Type->define(class_name => $name, is => ['UR::Namespace'], is_abstract => 0); my $namespace_src = $namespace->resolve_module_header_source; # Step 2 - Make an empty Vocabulary my $vocab_name = $name->get_vocabulary(); my $vocab = UR::Object::Type->define( class_name => $vocab_name, is => 'UR::Vocabulary', is_abstract => 0, ); my $vocab_src = $vocab->resolve_module_header_source(); my $vocab_filename = $vocab->module_base_name(); # write the namespace module $self->status_message("A $name (UR::Namespace)\n"); IO::File->new("$name.pm", 'w')->printf($module_template, $name, $namespace_src); # Write the vocbaulary module mkdir($name); IO::File->new($vocab_filename,'w')->printf($module_template, $vocab_name, $vocab_src); $self->status_message("A $vocab_name (UR::Vocabulary)\n"); # Step 3 - Make and write a new Meta DataSource module # and also, the SQL source for a new, empty metadata DB my ($meta_datasource, $meta_db_file) = UR::DataSource::Meta->generate_for_namespace($name); my $meta_datasource_name = $meta_datasource->id; $self->status_message("A $meta_datasource_name (UR::DataSource::Meta)\n"); $self->status_message("A $meta_db_file (Metadata DB skeleton)"); return 1; } 1; Init.pm100664023532023421 342612544604516 17451 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Init; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "Command", has => [ namespace => { is => 'Text', shell_args_position => 1, doc => 'the name of the namespace/app to create' }, db => { is => 'Text', is_optional => 1, shell_args_position => 2, doc => 'the (optional) DBI connection string for the primary data source', }, ], doc => 'create a new ur app with default classes in place', ); sub sub_command_sort_position { 1 } sub execute { my $self = shift; my $c; my $t = UR::Context::Transaction->begin(); $self->status_message("*** ur define namespace " . $self->namespace); UR::Namespace::Command::Define::Namespace->execute(nsname => $self->namespace)->result or die; if ($self->db) { $self->status_message("\n*** cd " . $self->namespace); chdir $self->namespace or ($self->error_message("error changing to namespace dir? $!") and die); $self->status_message("\n*** ur define db " . $self->db); $c = UR::Namespace::Command::Define::Db->create(uri => $self->db) or return; $c->dump_status_messages(1); $c->execute() or die; $self->status_message("\n*** ur update classes-from-db"); $c = UR::Namespace::Command::Update::ClassesFromDb->create(); $c->dump_status_messages(1); $c->execute() or die; } else { $self->status_message("next: cd " . $self->namespace); $self->status_message("then: ur define db DSN"); $self->status_message("then: ur update classes-from-db"); } $t->commit; return 1; } 1; List.pm100664023532023421 45312544604516 17436 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::List; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => "list objects, classes, modules" ); sub sub_command_sort_position { 5 } 1; Classes.pm100664023532023421 65212544604516 21034 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/List package UR::Namespace::Command::List::Classes; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_description { "List all classes in the current namespace." } sub for_each_class_object { my $self = shift; my $class = shift; print $class->class_name,"\n"; } 1; Modules.pm100664023532023421 63612544604516 21051 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/List package UR::Namespace::Command::List::Modules; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_description { "List all modules in the current namespace." } sub for_each_module_file { my $self = shift; my $module = shift; print "$module\n"; } 1; Objects.pm100664023532023421 65212544604516 21030 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Listpackage UR::Namespace::Command::List::Objects; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use above "UR"; use UR::Object::Command::List; class UR::Namespace::Command::List::Objects { is => 'UR::Object::Command::List', }; 1; #$HeadURL: svn+ssh://svn/srv/svn/gscpan/distro/ur-bundle/trunk/lib/UR/Namespace/Command/List/Objects.pm $ #$Id: Objects.pm 36327 2008-07-08 20:59:29Z ebelter $ Old.pm100664023532023421 56612544604516 17246 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Old; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => "deprecated commands for namespaces, data sources and classes", ); sub _is_hidden_in_docs { 1 } sub shell_args_description { "[namespace|...]"; } 1; DiffRewrite.pm100664023532023421 71212544604516 21451 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::DiffRewrite; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", ); sub help_description { "Show the differences between current class headers and the results of a rewrite." } *for_each_class_object = \&UR::Namespace::Command::Diff::for_each_class_object_delegate_used_by_sub_commands; 1; DiffUpdate.pm100664023532023421 66512544604516 21261 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::DiffUpdate; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", ); sub help_description { "Show the differences between class schema and database schema." } *for_each_class_object = \&UR::Namespace::Command::Diff::for_each_class_object_delegate_used_by_sub_commands; 1; ExportDbicClasses.pm100664023532023421 344712544604516 22650 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Oldpackage UR::Namespace::Command::Old::ExportDbicClasses; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Create or update a DBIx::Class class from an already existing UR class"; } sub help_detail { return <bare_args) { $self->error_message("No class names were specified on the command line"); $self->status_message($self->help_usage_complete_text,"\n"); return; } my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } eval "use $namespace"; if ($@) { $self->error_message("Failed to load namespace $namespace"); return; } foreach my $class_name ( $self->bare_args ) { my $class = UR::Object::Type->get(class_name => $class_name); unless ($class) { $self->error_message("Couldn't load class metadata for $class_name"); next; } $class->dbic_rewrite_module_header(); } return 1; } sub for_each_class_object { my($self,$class) = @_; return 1 unless ($class->table_name); # Skip classes without tables $class->dbic_rewrite_module_header(); return 1; } 1; Info.pm100664023532023421 642412544604516 20160 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Oldpackage UR::Namespace::Command::Old::Info; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ subject => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Outputs description(s) of UR entities such as classes and tables to stdout"; } sub is_sub_command_delegator { 0;} sub execute { my($self, $params) = @_; my $namespace = $self->namespace_name; # FIXME why dosen't require work here? eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } # Loop through each command line parameter and see what kind of thing it is # create a view and display it my @class_aspects = qw( ); my @table_aspects = qw( ); my %already_printed; my %views; foreach my $item ( $self->subject ) { my @meta_objs = (); if ($item eq $namespace or $item =~ m/::/) { # Looks like a class name? my $class_meta = eval { UR::Object::Type->get(class_name => $item)}; push(@meta_objs, $class_meta) if $class_meta; } else { push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => $item, namespace => $namespace) ); push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => uc($item), namespace => $namespace) ); push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => lc($item), namespace => $namespace) ); push @meta_objs, map { ( $_ and UR::DataSource::RDBMS::Table->get(table_name => $_->table_name, namespace => $namespace) ) } ( UR::DataSource::RDBMS::TableColumn->get(column_name => $item, namespace => $namespace), UR::DataSource::RDBMS::TableColumn->get(column_name => uc($item), namespace => $namespace), UR::DataSource::RDBMS::TableColumn->get(column_name => lc($item), namespace => $namespace) ); } ## A property search requires loading all the classes first, at least until class ## metadata is in the meta DB # Something is making this die, so I'll comment it out for now #$namespace->get_material_class_names; #my @properties = UR::Object::Property->get(property_name => $item); #next unless @properties; #push @meta_objs, UR::Object::Type->get(class_name => [ map { $_->class_name } # @properties ]); foreach my $obj ( @meta_objs ) { next unless $obj; next if ($already_printed{$obj}++); $views{$obj->class} ||= UR::Object::View->create( subject_class_name => $obj->class, perspective => 'default', toolkit => 'text', ); my $view = $views{$obj->class}; $view->subject($obj); $view->show(); print "\n"; } } } 1; Redescribe.pm100664023532023421 114012544604516 21322 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::Redescribe; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', ); sub help_brief { "Outputs class description(s) formatted to the latest standard." } sub for_each_class_object { my $self = shift; my $class = shift; my $src = $class->resolve_module_header_source; if ($src) { print $src, "\n"; return 1; } else { print STDERR "No source for $class!"; return; } } 1; RunsOnModulesInTree.pm100664023532023421 1400712544604516 22447 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command # Abstract base command for commands which run on all or part of a class tree. package UR::Namespace::Command::RunsOnModulesInTree; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ classes_or_modules => { is_many => 1, is_optional => 1, shell_args_position => 99 } ] ); sub is_abstract { my $self = shift; my $class = ref($self) || $self; return 1 if $class eq __PACKAGE__; return; } sub _help_detail_footer { my $text = return <namespace_name; unless ($namespace) { die "This command can only be run from a directory tree under a UR namespace module.\n"; } my @subject_list = $self->classes_or_modules; if ($self->can("for_each_class_object") ne __PACKAGE__->can("for_each_class_object")) { my @classes = $self->_class_objects_in_tree(@subject_list); unless ($self->before(\@classes)) { print STDERR "Terminating.\n"; return; } for my $class (@classes) { unless ($self->for_each_class_object($class)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_class_name") ne __PACKAGE__->can("for_each_class_name")) { my @class_names = $self->_class_names_in_tree(@subject_list); unless ($self->before(\@class_names)) { print STDERR "Terminating.\n"; return; } for my $class (@class_names) { unless ($self->for_each_class_name($class)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_module_file") ne __PACKAGE__->can("for_each_module_file")) { my @modules = $self->_modules_in_tree(@subject_list); unless ($self->before(\@modules)) { print STDERR "Terminating.\n"; return; } for my $module (@modules) { unless ($self->for_each_module_file($module)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_module_file_in_parallel") ne __PACKAGE__->can("for_each_module_file_in_parallel")) { my @modules = $self->_modules_in_tree(@subject_list); unless ($self->before(\@modules)) { print STDERR "Terminating.\n"; return; } my $bucket_count = 10; my @buckets; my %child_processes; for my $bucket_number (0..$bucket_count-1) { $buckets[$bucket_number] ||= []; } while (@modules) { for my $bucket_number (0..$bucket_count-1) { my $module = shift @modules; last if not $module; push @{ $buckets[$bucket_number] }, $module; } } for my $bucket (@buckets) { my $child_pid = fork(); if ($child_pid) { # the parent process continues forking... $child_processes{$child_pid} = 1; } else { # the child process does handles its bucket for my $module (@$bucket) { unless ($self->for_each_module_file_in_parallel($module)) { exit 1; } } # and then exits quietly exit 0; } } #$DB::single = 1; while (keys %child_processes) { my $child_pid = wait(); if ($child_pid == -1) { print "lost children? " . join(" ", keys %child_processes); } delete $child_processes{$child_pid}; } } else { die "$self does not implement: for_each_[class_object|class_name|module_file]!"; } unless ($self->after()) { print STDERR "Terminating.\n"; return; } return 1; } sub before { return 1; } sub for_each_module_file { die "The for_each_module_file method is not defined by/in " . shift; } sub for_each_class_name { die "The for_each_class_name method is not defined by/in " . shift; } sub for_each_class_object { Carp::confess "The for_each_class_object method is not defined by/in " . shift; } sub after { return 1; } sub loop_methods { my $self = shift; my @methods; for my $method (qw/ for_each_class_object for_each_class_name for_each_module_file for_each_module_file_in_parallel /) { no warnings; if ($self->can($method) ne __PACKAGE__->can($method)) { push @methods, $method; } } return @methods; } sub shell_args_description { my $self = shift; my @loop_methods = $self->loop_methods; my $takes_classes = 1 if grep { /class/ } @loop_methods; my $takes_modules = 1 if grep { /modul/ } @loop_methods; my $text; if ($takes_classes and $takes_modules) { $text = "[CLASS|MODULE] [CLASS|MODULE] ..."; } elsif ($takes_classes) { $text = "[CLASS] [CLASS].."; } elsif ($takes_modules) { $text = "[MODULE] [MODULE] ..."; } else { $text = ""; } $text .= " " . $self->SUPER::shell_args_description(@_); if ($self->is_sub_command_delegator) { my @names = $self->sub_command_names; return "[" . join("|",@names) . "] $text" } return $text; } 1; Show.pm100664023532023421 27512544604516 17445 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Show; use strict; use warnings; class UR::Namespace::Command::Show { is => 'Command::Tree', doc => 'show data about classes, data storage', }; 1; Properties.pm100664023532023421 535712544604516 21627 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Properties; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ classes_or_modules => { is_optional => 0, is_many => 1, shell_args_position => 99, doc => 'classes to describe by class name or module path', }, ], doc => 'show class properties, relationships, meta-data', ); sub sub_command_sort_position { 3 } sub help_synopsis { return <create_view( perspective => 'default', toolkit => 'text', aspects => [ 'namespace', 'table_name', 'data_source_id', 'is_abstract', 'is_final', 'is_singleton', 'is_transactional', 'schema_name', 'meta_class_name', { label => 'Inherits from', name => 'ancestry_class_names', }, { label => 'Properties', name => 'properties', subject_class_name => 'UR::Object::Property', perspective => 'description line item', toolkit => 'text', aspects => ['is_id', 'property_name', 'column_name', 'data_type', 'is_optional' ], }, { label => "References", name => 'all_id_by_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'reference description', toolkit => 'text', aspects => [], }, { label => "Referents", name => 'all_reverse_as_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'reference description', toolkit => 'text', aspects => [], }, ], ); unless ($view) { $self->error_message("Can't initialize view"); return; } $view->subject($class_meta); $view->show(); } 1; Schema.pm100664023532023421 302312544604516 20657 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Schema; use strict; use warnings; class UR::Namespace::Command::Show::Schema { is => 'Command::V2', has_input => [ class_names => { is => 'Text', is_many => 1, shell_args_position => 1, require_user_verify => 0, doc => 'dump the required database schema changes for a class or classes' }, complete => { is => 'Boolean', default_value => 0, doc => 'when set, dump the complete table creation command not just the required changes', }, ], doc => 'database DDL', }; sub execute { my $self = shift; my @class_names = $self->class_names; $ENV{UR_DBI_NO_COMMIT} = 1; my $t = UR::Context::Transaction->begin; $DB::single = 1; for my $class_name (@class_names) { my $meta = $class_name->__meta__; my $class_name = $meta->class_name; $self->status_message("-- class $class_name\n"); my $ds = $meta->data_source; my @schema_objects = $ds->generate_schema_for_class_meta($meta,1); my ($tt) = grep { $_->isa("UR::DataSource::RDBMS::Table") } @schema_objects; my @ddl = $ds->_resolve_ddl_for_table($tt, all => 1); if (@ddl) { my $ddl = join("\n",@ddl); $self->status_message($ddl); } else { $self->status_message("-- no changes for $class_name, run with the 'complete' flag for the full table DDL"); } } $t->rollback; return 1; } 1; Subclasses.pm100664023532023421 1552112544604516 21614 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Subclasses; use strict; use warnings; use UR; use YAML; my $spacing = ''; class UR::Namespace::Command::Show::Subclasses { is => 'Command::V2', has=> [ superclass => { is => 'Text', shell_args_position => 1, doc => 'Only show subclasses of this class.', }, color => { is => 'Boolean', is_optional => 1, default_value => 1, doc => 'Display in color.', }, maximum_depth => { is => 'Int', is_optional => 1, default_value => -1, doc => 'Maximum subclass depth. Negative means infinite.', }, recalculate => { is => 'Boolean', is_optional => 1, default_value => 0, doc => 'Recreate the cache instead of using the results of a previous run.', }, flat => { is => 'Boolean', is_optional => 1, doc => 'Simply prints the subclass names with no other formatting or coloring.', } ], doc => 'Display subclasses of a given class.', }; sub help_synopsis { my $self = shift; my $result .= < ur show subclasses EOP return $result; } sub _mine_tree_for_class { my ($tree, $name, $result) = @_; if(ref($tree) eq 'HASH') { for my $key (keys %{$tree}) { if($key eq $name) { push(@{$result}, 1); } else { _mine_tree_for_class($tree->{$key}, $name, $result); } } } elsif(ref($tree) eq 'ARRAY') { for my $item (@{$tree}) { if($item eq $name) { push(@{$result}, 1); } _mine_tree_for_class($item, $name, $result); } } return; } sub execute { my ($self) = @_; my $indexfile = '/tmp/.ur_class_index'; my $subclass_index_ref; if($self->recalculate or (not -e $indexfile)) { my $test_use_cmd = UR::Namespace::Command::Test::Use->create(); $test_use_cmd->execute(); $subclass_index_ref = {}; create_subclass_index('UR::Object', $subclass_index_ref); my %subclass_index = %{$subclass_index_ref}; open(my $output_fh, '>', $indexfile); for my $key (keys %subclass_index) { print $output_fh sprintf("%s %s\n", $key, join("\t", @{$subclass_index{$key}})); } close($output_fh); } else { $subclass_index_ref = parse_subclass_index_file($indexfile); } # check to see if superclass is even in the subclass_index my @result; _mine_tree_for_class($subclass_index_ref, $self->superclass, \@result); unless(@result) { my $class_name = $self->color ? Term::ANSIColor::colored($self->superclass, 'red') : $self->superclass; printf "%s is not a valid class, check your spelling or " . "see --help (recalculate).\n", $class_name; return; } if($self->flat) { $self->display_subclasses_flat($subclass_index_ref, $self->superclass, 0) } else { $self->display_subclasses($subclass_index_ref, $self->superclass, '', ' ', 0); } return 1; } sub create_subclass_index { my ($seed, $index_ref) = @_; my @children = $seed->__meta__->subclasses_loaded; for my $child (@children) { my @parents = @{$child->__meta__->{is}}; for my $parent (@parents) { if($index_ref->{$parent}) { push(@{$index_ref->{$parent}}, $child); } else { $index_ref->{$parent} = [$child]; } } } } sub parse_subclass_index_file { my ($indexfile) = @_; open(IN, '<', $indexfile); my %index; while(my $line = ) { chomp($line); if($line) { my ($parent, $rest) = split(/ /, $line); if($rest) { my @children = split('\t', $rest); $index{$parent} = \@children; } else { $index{$parent} = []; } } } return \%index } sub display_subclasses_flat { my ($self, $index_ref, $name, $depth) = @_; my $maximum_depth = $self->maximum_depth; if($depth == $maximum_depth + 1 and $maximum_depth != -1) { return; } print "$name\n"; # get the children my $children_ref = $index_ref->{$name}; my @children; if($children_ref) { @children = @{$index_ref->{$name}}; } else { # if it isn't in index it has no children. @children = (); } # loop over children for my $child (@children) { $self->display_subclasses_flat($index_ref, $child, $depth+1); } } sub display_subclasses { my ($self, $index_ref, $name, $global_prefix, $personal_prefix, $depth) = @_; my $maximum_depth = $self->maximum_depth; my ($dgp, $dpp, $dn) = ($global_prefix, $personal_prefix, $name); if($self->color) { ($dgp, $dpp, $dn) = colorize_output($global_prefix, $personal_prefix, $name, $self->superclass); } print join('', $dgp, $dpp, $spacing, $dn); my $o = ($personal_prefix =~ /^\|/ ) ? '|' : ' '; my $child_global_prefix = sprintf("%s%s %s", $global_prefix, $o, $spacing); # get the children my $children_ref = $index_ref->{$name}; my @children; if($children_ref) { @children = @{$index_ref->{$name}}; } else { # if it isn't in index it has no children. @children = (); } # loop over children my $len_children = scalar(@children); if($len_children and $depth == $maximum_depth and $maximum_depth != -1) { print " ...\n"; return; } print "\n"; my $i = 1; for my $child (@children) { my $child_personal_prefix = ($len_children == $i) ? '`-' : '|-'; $self->display_subclasses($index_ref, $child, $child_global_prefix, $child_personal_prefix, $depth+1); $i += 1; } } sub colorize_output { my ($global_prefix, $personal_prefix, $name, $superclass) = @_; my $dgp = Term::ANSIColor::colored($global_prefix, 'white'); my $dpp = Term::ANSIColor::colored($personal_prefix, 'white'); my $name_prefix = $name; if($name_prefix =~ /^($superclass)/) { $name_prefix = $superclass; } else { $name_prefix = ''; } my $name_suffix = $name; $name_suffix =~ s/^($superclass)//; my $dn = sprintf("%s%s", Term::ANSIColor::colored($name_prefix, 'white'), $name_suffix ); return ($dgp, $dpp, $dn); } 1; Sys.pm100664023532023421 36712544604516 17305 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Sys; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => 'service launchers' ); 1; ClassBrowser.pm100664023532023421 5254212544604516 21760 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Syspackage UR::Namespace::Command::Sys::ClassBrowser; # This turns on the perl stuff to insert data in the DB # namespace so we can get line numbers and stuff about # loaded modules BEGIN { unless ($^P) { no strict 'refs'; *DB::DB = sub {}; $^P = 0x31f; } } use strict; use warnings; use UR; use Data::Dumper; use File::Spec; use File::Basename; use IO::File; use Template; use Plack::Request; use Class::Inspector; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has_optional => [ generate_cache => { is => 'Boolean', default_value => 0, doc => 'Generate the class cache file' }, use_cache => { is => 'Boolean', default_value => 1, doc => 'Use the class cache instead of scanning for modules'}, port => { is => 'Integer', default_value => 8080, doc => 'TCP port to listen for connections' }, timeout => { is => 'Integer', doc => 'If specified, exit after this many minutes of inactivity' }, host => { is => 'String', default_value => 'localhost', doc => 'host to listen on for connections' }, ], ); sub is_sub_command_delegator { 0;} sub help_brief { "Start a web server to browse through the class structure."; } sub help_synopsis { q(# Start the web server # By default, only connections from localhost are accepted ur sys class-browser # Start the server and accept connections from any # address, not just localhost ur sys class-browser --host 0 # Create the cache file for the current namespace ur sys class-browser --generate-cache); } sub help_detail { q(The class-browser command starts an embedded web server containing an app for browsing throught the class structure. After starting, it prints a URL on STDOUT that can be copy-and-pasted into a browser to run the app.); } sub _class_info_cache_file_name_for_namespace { my($self, $namespace) = @_; unless ($INC{$namespace.'.pm'}) { eval "use $namespace"; die $@ if $@; } my $class_cache_file = sprintf('.%s-class-browser-cache', $namespace); return File::Spec->catfile($namespace->get_base_directory_name, $class_cache_file); } sub load_class_info_for_namespace { my($self, $namespace) = @_; my $class_cache_file = $self->_class_info_cache_file_name_for_namespace($namespace); if ($self->use_cache and -f $class_cache_file) { $self->_load_class_info_from_cache_file($namespace, $class_cache_file); } else { $self->status_message("Preloading class information for namespace $namespace..."); $self->_load_class_info_from_modules_on_filesystem($namespace); } } sub _write_class_info_to_cache_file { my $self = shift; my $current_namespace = $self->namespace_name; return unless ($self->{_cache}->{$current_namespace}); my $cache_file = $self->_class_info_cache_file_name_for_namespace($current_namespace); my $fh = IO::File->new($cache_file, 'w') || die "Can't open $cache_file for writing: $!"; $fh->print( Data::Dumper->new([$self->{_cache}->{$current_namespace}], ['cache_data'])->Sortkeys(1)->Purity(1)->Dump ); $fh->close(); $self->status_message("Saved class info to cache file $cache_file"); } sub _load_class_info_from_cache_file { my($self, $namespace, $class_cache_file) = @_; return 1 if ($self->{_cache}->{$namespace}); # Don't load same namespace more than once $self->status_message("Loading class info cache file $class_cache_file\n"); my $fh = IO::File->new($class_cache_file, 'r'); unless ($fh) { $self->error_message("Cannot load class cache file $class_cache_file: $!"); return; } my $buf; { local $/; $buf = <$fh>; } my $cache_data; eval $buf; $self->{_cache}->{$namespace} = $cache_data; } sub _load_class_info_from_modules_on_filesystem { my $self = shift; my $namespace = shift; return 1 if ($self->{_cache}->{$namespace}); # Don't load same namespace more than once my $by_class_name = $self->{_cache}->{$namespace}->{by_class_name} ||= $self->_generate_class_name_cache($namespace); unless ($self->name_tree_cache($namespace)) { $self->name_tree_cache( $namespace, UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new( name => $namespace, relpath => $namespace.'.pm')); } unless ($self->inheritance_tree_cache($namespace)) { $self->inheritance_tree_cache( $namespace, UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new( name => 'UR::Object', relpath => 'UR::Object')); } unless ($self->directory_tree_cache($namespace)) { $self->directory_tree_cache($namespace, UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new( name => $namespace, relpath => $namespace.'.pm' )); } my $inh_inserter = $self->_class_inheritance_cache_inserter($by_class_name, $self->inheritance_tree_cache($namespace)); foreach my $data ( values %$by_class_name ) { $self->_insert_cache_for_class_name_tree($data); $self->_insert_cache_for_path($data); $inh_inserter->($data->{name}); } 1; } foreach my $cache ( [ 'by_class_name_tree', 'name_tree_cache'], [ 'by_class_inh_tree', 'inheritance_tree_cache'], [ 'by_directory_tree', 'directory_tree_cache'] ) { my $key = $cache->[0]; my $subname = $cache->[1]; my $sub = sub { my $self = shift; my $namespace = shift; unless (defined $namespace) { Carp::croak "\$namespace is a required argument"; } if (@_) { $self->{_cache}->{$namespace}->{$key} = shift; } return $self->{_cache}->{$namespace}->{$key}; }; Sub::Install::install_sub({ into => __PACKAGE__, as => $subname, code => $sub, }); } sub _namespace_for_class_name { my($self, $class_name) = @_; return ($class_name =~ m/^(\w+)(::)?/)[0]; } sub _cached_data_for_class { my($self, $class_name) = @_; my $namespace = $self->_namespace_for_class_name($class_name); return $self->{_cache}->{$namespace}->{by_class_name}->{$class_name}; } # 1-level hash. Maps a class name to a hashref containing simple # data about that class. relpath is relative to the namespace's module_path sub _generate_class_name_cache { my($self, $namespace) = @_; my $cwd = Cwd::getcwd . '/'; my $namespace_meta = $namespace->__meta__; my $namespace_dir = $namespace_meta->module_directory; (my $path = $namespace_meta->module_path) =~ s/^$cwd//; my $by_class_name = { $namespace => { name => $namespace, is => $namespace_meta->is, relpath => $namespace . '.pm', id => $path, file => File::Basename::basename($path), } }; foreach my $class_meta ( $namespace->get_material_classes ) { my $class_name = $class_meta->class_name; $by_class_name->{$class_name} = $self->_class_name_cache_data_for_class_name($class_name); } return $by_class_name; } sub _class_name_cache_data_for_class_name { my($self, $class_name) = @_; my $class_meta = $class_name->__meta__; unless ($class_meta) { Carp::carp("Can't get class metadata for $class_name... skipping."); return; } my $namespace_dir = $class_meta->namespace->__meta__->module_directory; my $module_path = $class_meta->module_path; (my $relpath = $module_path) =~ s/^$namespace_dir//; return { name => $class_meta->class_name, relpath => $relpath, file => File::Basename::basename($relpath), is => $class_meta->is, }; } # Build the by-class-name tree data sub _insert_cache_for_class_name_tree { my($self, $data) = @_; my $namespace = $self->_namespace_for_class_name($data->{name}); my $tree = $self->name_tree_cache($namespace); my @names = split('::', $data->{name}); my $relpath = shift @names; # Namespace is first part of the name while(my $name = shift @names) { $relpath = join('::', $relpath, $name); $tree = $tree->get_child($name) || $tree->add_child( name => $name, relpath => $relpath); } $tree->data($data); return $tree; } # Build the by_directory_tree data sub _insert_cache_for_path { my($self, $data) = @_; my $namespace = $self->_namespace_for_class_name($data->{name}); my $tree = $self->directory_tree_cache($namespace); # split up the path to the module relative to the namespace directory my @path_parts = File::Spec->splitdir($data->{relpath}); shift @path_parts if $path_parts[0] eq '.'; # remove . at the start of the path my $partial_path = shift @path_parts; while (my $subdir = shift @path_parts) { $partial_path = join('/', $partial_path, $subdir); $tree = $tree->get_child($subdir) || $tree->add_child( name => $subdir, relpath => $partial_path); } $tree->data($data); return $tree; } sub _cache_has_data_for { my($self, $namespace) = @_; return exists($self->{_cache}->{$namespace}); } # build the by_class_inh_tree data sub _class_inheritance_cache_inserter { my($self, $by_class_name, $tree) = @_; my $cache = $tree ? { $tree->name => $tree } : {}; my $do_insert; $do_insert = sub { my $class_name = shift; $by_class_name->{$class_name} ||= $self->_class_name_cache_data_for_class_name($class_name); my $data = $by_class_name->{$class_name}; if ($cache->{$class_name}) { return $cache->{$class_name}; } my $node = UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new( name => $class_name, data => $data ); $cache->{$class_name} = $node; if ((! $data->{is}) || (! @{ $data->{is}} )) { # no parents?! This _is_ the root! return $tree = $node; } foreach my $parent_class ( @{ $data->{is}} ) { my $parent_class_tree = $do_insert->($parent_class); unless ($parent_class_tree->has_child($class_name)) { $parent_class_tree->add_child( $node ); } } return $node; }; return $do_insert; } sub execute { my $self = shift; if ($self->generate_cache) { $self->_load_class_info_from_modules_on_filesystem($self->namespace_name); $self->_write_class_info_to_cache_file(); return 1; } $self->load_class_info_for_namespace($self->namespace_name); my $tt = $self->{_tt} ||= Template->new({ INCLUDE_PATH => $self->_template_dir, RECURSION => 1 }); my $server = UR::Service::WebServer->create(timeout => $self->timeout, host => $self->host, port => $self->port); my $router = UR::Service::UrlRouter->create( verbose => $self->verbose); my $assets_dir = $self->__meta__->module_data_subdirectory.'/assets/'; $router->GET(qr(/assets/(.*)), $server->file_handler_for_directory( $assets_dir)); $router->GET('/', sub { $self->index(@_) }); $router->GET(qr(/detail-for-class/(.*)), sub { $self->detail_for_class(@_) }); $router->GET(qr(/search-for-class/(.*)), sub { $self->search_for_class(@_) }); $router->GET(qr(/render-perl-module/(.*)), sub { $self->render_perl_module(@_) }); $router->GET(qr(/property-metadata-list/(.*)/(\w+)), sub { $self->property_metadata_list(@_) }); $server->cb($router); $server->run(); return 1; } sub _template_dir { my $self = shift; return $self->__meta__->module_data_subdirectory(); } sub index { my $self = shift; my $env = shift; my $req = Plack::Request->new($env); my $namespace = $req->param('namespace') || $self->namespace_name; unless ($self->_cache_has_data_for($namespace)) { $self->load_class_info_for_namespace($namespace); } my $data = { current_namespace => $namespace, namespaces => [ map { $_->id } UR::Namespace->is_loaded() ], classnames => $self->name_tree_cache($namespace), inheritance => $self->inheritance_tree_cache($namespace), paths => $self->directory_tree_cache($namespace), }; return $self->_process_template('class-browser.html', $data); } sub _process_template { my($self, $template_name, $template_data) = @_; my $out = ''; my $tmpl = $self->{_tt}; $tmpl->process($template_name, $template_data, \$out) and return [ 200, [ 'Content-Type' => 'text/html' ], [ $out ]]; # Template error :( $self->error_message("Template failed: ".$tmpl->error); return [ 500, [ 'Content-Type' => 'text/plain' ], [ 'Template failed', $tmpl->error ]]; } sub _fourohfour { return [ 404, [ 'Content-Type' => 'text/plain' ], ['Not Found']]; } sub _line_for_function { my($self, $name) = @_; my $info = $DB::sub{$name}; return () unless $info; my ($file,$start); if ($info =~ m/\[(.*?):(\d+)\]/) { # This should match eval's and __ANON__s ($file,$start) = ($1,$2); } elsif ($info =~ m/(.*?):(\d+)-(\d+)$/) { ($file,$start) = ($1,$2); } if ($start) { # Convert $file into a package name foreach my $inc ( keys %INC ) { if ($INC{$inc} eq $file) { (my $pkg = $inc) =~ s/\//::/g; $pkg =~ s/\.pm$//; return (package => $pkg, line => $start); } } } return; } # Return a list of package names where $method is defined sub _overrides_for_method { my($self, $class, $method) = @_; my %seen; my @results; my @isa = ($class); while (my $target_class = shift @isa) { next if $seen{$target_class}++; if (Class::Inspector->function_exists($target_class, $method)) { push @results, $target_class; } { no strict 'vars'; push @isa, eval '@' . $target_class . '::ISA'; } } return \@results; } sub detail_for_class { my $self = shift; my $env = shift; my $class = shift; my $class_meta = eval { $class->__meta__}; my $tree = UR::Namespace::Command::Sys::ClassBrowser::TreeItem->new( name => 'UR::Object', relpath => 'UR::Object'); my $namespace = $class_meta->namespace; my $treebuilder = $self->_class_inheritance_cache_inserter( $self->{_cache}->{$namespace}->{by_class_name}, $tree, ); $treebuilder->($class); unless ($class_meta) { return $self->_fourohfour; } my @public_methods = sort { $a->[2] cmp $b->[2] } # sort by function name @{ Class::Inspector->methods($class, 'public', 'expanded') }; my @private_methods = sort { $a->[2] cmp $b->[2] } # sort by function name @{ Class::Inspector->methods($class, 'private', 'expanded') }; # Convert each of them to a hashref for easier access foreach ( @public_methods, @private_methods ) { my $class = $_->[1]; my $method = $_->[2]; my $function = $_->[0]; my $cache = $self->_cached_data_for_class($class); $_ = { class => $class, method => $method, file => $cache->{relpath}, overrides => $self->_overrides_for_method($class, $method), $self->_line_for_function($function), }; } my @sorted_properties = sort { $a->property_name cmp $b->property_name } $class_meta->properties; my $tmpl_data = { meta => $class_meta, property_metas => \@sorted_properties, class_inheritance_tree => $tree, public_methods => \@public_methods, private_methods => \@private_methods, }; return $self->_process_template('class-detail.html', $tmpl_data); } sub search_for_class { my $self = shift; my $env = shift; my $search = shift; my $req = Plack::Request->new($env); my $namespace = $req->param('namespace') || $self->namespace_name; my $class_cache = $self->{_cache}->{$namespace}->{by_class_name}; my @results = sort grep { m/$search/i } keys %$class_cache; if (@results == 1) { return $self->detail_for_class($env, $results[0]); } else { return $self->_process_template('search_results.html', { search => $search, classes => \@results }); } } sub render_perl_module { my($self, $env, $module_name) = @_; my $module_path; if (my $class_meta = eval { $module_name->__meta__ }) { $module_path = $class_meta->module_path; } else { ($module_path = $module_name) =~ s/::/\//g; $module_path = $INC{$module_path.'.pm'}; } unless ($module_path and -f $module_path) { return $self->_fourohfour; } my $fh = IO::File->new($module_path, 'r'); my @lines = <$fh>; chomp(@lines); return $self->_process_template('render-perl-module.html', { module_name => $module_name, lines => \@lines }); } # Render the popover content when hovering over a row in the # class property table sub property_metadata_list { my($self, $env, $class_name, $property_name) = @_; my $class_meta = $class_name->__meta__; unless ($class_meta) { return $self->_fourohfour; } my $prop_meta = $class_meta->property_meta_for_name($property_name); unless ($prop_meta) { return $self->_fourohfour; } return $self->_process_template('partials/property_metadata_list.html', { meta => $prop_meta, show => [qw( doc class_name column_name data_type data_length is_id via to where reverse_as id_by valid_values example_values is_optional is_transient is_constant is_mutable is_delegated is_abstract is_many is_deprecated is_calculated calculate_perl calculate_sql )], }); } package UR::Namespace::Command::Sys::ClassBrowser::TreeItem; sub new { my $class = shift; my %node = @_; die "new() requires a 'name' parameter" unless (exists $node{name}); $node{children} = {}; unless (defined $node{id}) { ($node{id} = $node{name}) =~ s/::/__/g; } my $self = bless \%node, __PACKAGE__; return $self; } sub id { return shift->{id}; } sub name { return shift->{name}; } sub relpath { return shift->{relpath}; } sub data { my $self = shift; if (@_) { $self->{data} = shift; } return $self->{data}; } sub has_children { my $self = shift; return %{$self->{children}}; } sub children { my $self = shift; return [ values(%{$self->{children}}) ]; } sub has_child { my $self = shift; my $child_name = shift; return exists($self->{children}->{$child_name}); } sub get_child { my $self = shift; my $child_name = shift; return $self->{children}->{$child_name}; } sub add_child { my $self = shift; my $child = ref($_[0]) ? shift(@_) : $self->new(@_); $self->{children}->{ $child->name } = $child; } 1; =pod =head1 NAME UR::Namespace::Command::Sys::ClassBrowser - WebApp for browsing the class structure =head1 SYNOPSIS # Start the web server ur sys class-browser # Create the cache file for the current namespace ur sys class-browser --generate-cache =head1 DESCRIPTION The class-browser command starts an embedded web server containing an app for browsing throught the class structure. After starting, it prints a URL on STDOUT that can be copy-and-pasted into a browser to run the app. =head1 COMMAND-LINE OPTIONS With no options, the command expects to be run within a Namespace directory. It will auto-discover all the classes in the Namespace, either from a previously created cache file, or by scanning all the perl modules within the Namespace's subdirectory. =over 4 =item --generate-cache Instead of starting a web server, the command will scan for all perl modules within the Namespace's subdirectory and create a file called .-class-browser-cache, then exit. This file will contain information about all the classes it found, which will improve the start-up time the next time the command is run. =item --port Change the TCP port the web server listens on. The default is 8080. =item --nouse-cache The command will use the cache file generated by the --generate-cache option if it finds one. When --nouse-cache is used, it will always scan for perl modules, and will ignore any cache that may be present. =item --verbose Causes the command to print the STDOUT the URLs loaded while it is running. =back =head1 SEE ALSO L, L, L =cut glyphicons-halflings-white.png100664023532023421 2111112544604516 31434 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/assets/img‰PNG  IHDRÕŸ˜Ó³{ÙPLTEÿÿÿùùùÿÿÿÿÿÿýýýmmmÿÿÿÿÿÿÿÿÿÿÿÿðððþþþöööüüüÿÿÿÿÿÿÚÚÚÂÂÂôôôÿÿÿÿÿÿôôô÷÷÷ÿÿÿ³³³ýýýâââ°°°ÿÿÿÿÿÿûûûçççþþþÿÿÿíííÏÏÏýýýöööíííûûûçççúúúááá’’’þþþþþþÁÁÁ˜˜˜tttáááÐÐÐóóó»»»¡¡¡€€€ýýýÔÔÔbbbÿÿÿÕÕÕøøøÜÜÜúúúûûûéééûûûýýýýýýÑÑÑòòòüüüøøøëëëüüü¶¶¶ÆÆÆåååîîîõõõýýýeeegggððð¶¶¶ààà÷÷÷úúúéééåååúúúøøøËËËÿÿÿ„„„ñññxxx÷÷÷ÝÝÝùùùÈÈÈÒÒÒìììúúúÞÞÞâââæææóóó›››¨¨¨¥¥¥ÜÜÜîîîÿÿÿñññÉÉÉðððÿÿÿÿÿÿÞÞÞÆÆÆ¼¼¼ëëëÖÖÖÐÐÐâââùùùôôôâââìììõõõ´´´ÿÿÿýýýûûûüüüúúúæææäääüüü÷÷÷°°°™™™ýýýìììüüüÁÁÁéééÿÿÿÚÚÚððððððõõõñññþþþøøøþþþŽŽŽâââûûûùùùÜÜÜÿÿÿòòòúúúŸŸŸííí÷÷÷öööèèèóóóúúúõõõõõõ¦¦¦ËËËúúúøøøÓÓÓëëëúúúëë몪ªóóóííí¢¢¢ÏÏÏÚÚÚÖÖÖ¢¢¢ëëëâââùùùUUUÍÍÍÿÿÿÖÖÖãããáááêêêüüüÿÿÿöööûûûóóóôôôÌÌÌÿÿÿÿÿÿùùùõõõÿÿÿòòòýýýÙÙÙüüüûûûüüüééé¿¿¿ûûûêêêéééþþþÿÿÿørOæòtRNSÔÏñ#ïŸ_ /©ðÆâ¿oS·ß?†ÅCá kD¯ÂÀOS_ ¥š²ŒÓ6Ðà>4!~a §@1Ñ_'oÄn¢Ò‹‘€M†¡“3±BQj™¶p&%!lµÃ"Xqr;€— A[‚<`‰am}4…3/0Iˆ¨PCM!6(*gK&YQ¦GDP,å`’{VP¤-êxÁ)hÝ7‡e1]ôˆßW¿³$—‡1ƒbÄzSÜ•cOÙÍ]ÀŠ–U;Zié»'yÜ"€âÐÐ‘ÝØ†‰K 64ÖYœ*.vè@²¸îŽc.};‡ïŸtN%¨DIª˜ÊÐ !Z¶Ð5LñH£2Ú6 ŒƒÉ¯ŽÖ"Š Ô-b±E,,)ÊÀ BŒ·¦>m¹ªÃãúnøö6pmŸRöO wm@°ÝÌVÝ#?É'C…È‘Z#©Žqž‡ìÀbÒÓ|$½:Ü)‰Â/E¾%÷ânR¹q—CàhnµÉ%õiÓÌ“­º¶¶ß}lƒm ?iÿdâdÃ"€,Ø­Ç`¬Hñ"r.z¡¼‹ŽÁ~ýìü(bðQÜU&ê½—)–5õêX#•§ òé™EMªæÜR<Í*p[€[%.©OÉÌ£˜¥k7“lIo°ý¨ý“¶ßJ°F  ¥lV!̡ăuH‚`Ƽ™€—›ç‚&¢,Çz´ÉRk$¤ò¨|$ölŠ»¼Xbü¢âéÇjߪÈdU±û?Σ$Hµî¸©W±¾$Uû'…ÆÅHÜE3*Õ­º€µµU\}ê­ý†(Ò ¤zhVk}gÇu«Rk$¤ò%¨|‰T¨|Úêck¦ç³"ãžä±Dç”ý«ƒ_W+‹®”Ê.QòÒÅ)Õ@«ý“ƽ€H¢À›Íbµs¸ÔlžŽT´©·Dÿô­RÄ2Xm£#a Ýêº3lYÃÎzÌj¹ŽÔã’š#!Þ 4þJ´Ä8Ñ(Œòcµv™‰¾t]­a·˜T™Çàø Ò÷D Î…à¼áQ?^-‹Õ_^$:\ÿìÞV  $«•N|ì=(vˆZ'q6¹Zð׆‡×üB5VìÌî!y†´¼3äßKœÿ㱿bàv4Œñxðëê£âR]al—í!ÔþIÛo‡P‰@Åt¥äVy”ºîàLïÿÙªmlµÚ¿I¨Ub|[*°¶lke'*¾WdîÀÝdà³ðïD·Ó}\W›ƒ_Wß´ù¶¤rÐNÚ?™øÛvÞ«ÁÛ²X%§Ž0u‡öoui*„üJV·€Æ¦‡b%†}ôãˆi5I¥YlNŸE-wÐÏ‚ûf_W3mþIåà…Äý“…—-ŒmƒÊ¬²Q)“S µÖk´«TC7êím¤<"ÄôÜŒ‡b‹T|ìÆ'¦Õ$µÒ˜Ÿ£óóÖR&>¥êO pœõºš¾ù…ê6ݬÒöçú½t±¨î¥S­ŽN\©×¯LŒîmÕø\ÈÎÑÊÄr@¦3žuT b7úÓt.5.q©ôÈ3²r0ü=™8T¿ªi­J©\ëÈ6uF ”²R¸32^÷íñ'ŪŠóÀí±xˆâI« ïÒF„8O{%8­žkJšÓMSÈ´dâBEdæÑè ïW‚CYÃ÷O:/OŒN/—I‹ê_=½€xFE”Ñ! Í=¥æi:oÁ~’¡· yþ?¶š'·š'·š[Í“[Í“[Í“[Í“[Í­–è».¹U>±$÷P–ƦŠc%†] Û\c©´:é| ý,e¯SœZ,‘oš¿XríäÎËXº!ëRæ”ÇÆò@áZøv‚ ‡0Ôç>?Á*ç® Ô<ðþÕ|ø«¼N6þ0ú¹;{¯ažd³ê2Ôév+Däó^tààúÑ[q!òÛžV}Èøf«œÛ¨ÏŽÎ×Yÿêeॗ€Ë)Vyl|" f÷UDzqˆ@ëˆÇ¼˜4Y-˜³YýÍ-!¶6a“žŠB:o%ñJ¤ÛI±´—UQ|£UÆK¨O `¢®=\ ý´­ò:ë0¾°Àx …±Paó‰Ìuˆ@œ»!ç»K†âPÏdÕxhw1>×$jγ“vöZdàè™xñ«ÕSšUAÅ&[URßd•ý7ðøÂz·ký«/˜œðr¢U^¬Žä £ó—w:I.àVÇ®ëôÿc>qí.!·zSÛr&«³Õ2…)Wgù ¾…R -ÎiãQ 8¿çØûPa\О×U%•iÝ¡¦þUï_=àÃpÊø ›Lu ê(îžN¹?†Ÿ 0?Æ:]½Î¬ä†ÔÏt¬B%“U|™úù²¡NsorNÿ¹f¶ú ø,»P !­v" Y¬6¼hLï_­@@…bé·s¯c¶¬£qg˜v4|Â|0lÏŸÐëÔ$SŒõ9ŽîòbʱšÑj#ŽŸ£~žƒÁÒÏ?o²÷}‘‘ƒð}7sAPm:IV¹=n•÷¯ !ôþÕ{±›{ÍÝh¼ÎEࢪ£8¤sèu€ÍoL®ëÈTð$ñ„õ;VÝú¹­sõöcqìD¦3ðø¸ñ üÛ༂3.D«Bˆ«éý«³B4Ì&ìV'ØÜ TÅ `õà½Dï6ÿ™žšÏ·óqýyùjû8V‰Õæ*ëÖíX%ý³›@s«\ÞjrNµ$à|ö=5þΆ 'ìmU«iý«Kýi€%C™ÉIð:ssaÆ…`*`óµ=úl½÷)>ÈuÕ˜MeuSš›·¨Iò_ÎO÷ˆLü£_©}o&©íÀjzÿêÝpèºþ{¨¤ÖáÜlu:OñÁ®«ÆÌ)«s¤%Q@ãÍ$Þ<]f› € xO%…÷PCbhr2£ÕôþÕ¼ŸèýPK·Êëpžf5½Në3^o«ù©ú¼]êe²JÊêÁ¤iÐB˜œ464†€^tï‡uÙ²þUÖŒ:G4'¿ò22YêpÎëˆÌu¦G'/PyÙ4?¡þè.ÕæSB„P_>‘ÑëšI 1t3Γ÷BäÉ­æÉ­æÉ­æÉ­æVóäVóäVóäVóäVs«æÃ]î³!×67(ªÇg ¯¤¥‹Šyƒ°@†” 4>QÚò ßÕV«F­}^XׇìÚ¼ˆ’Õjµ¦e÷26 Lž³Ð%žòY´Gâh û³šl‰C­}­)Óâ< ˆ!ÚE ôðÇE½PçZWZ™½ŒV+þ@†ÏR 5{@ou—Ɇ4²‚²&…„˜´H…Ѭ6÷eµy V‹ˆÝ€˜VÅ¥ÖÁ¬¾ácqZ„Þ’©rìÓJÆçyBêæyžÓˆFzÑõFN¢$¢HbÈÈÕ³*+jÕqòÑÎÀ Ú«˜kÝ¿UàX„¯lºe·ìÄö¾Ä1“ÕÊÚdà0d^õ-‘B%‰ƒ}ê œø¸{Yõ¡™%rÇ*Òj5Ak5¦u«³"Ì,·:~éÒ¸áY¾Ü~ h™÷ûÄSA~¿­6ì ¼fuÁlÕ‡fµŠ{ȵQtATHÐZˆkÀªŠÆ­/_°¸ÕSŸî¼náû¹ ±u']bù]|m`«B…ñÄÁÏÀ¡J,O$íÁdu]·Zs® ÀFLß:©Äùúú›aõø‹À‹Ç™ÕÂÌT4Ïoà~by?wpÇj滥ÖAœ…Ø(€xù]„†¦ú…ªfÕí¶~anÖ§/ž©¸¿^ÈdÕÚ²öcØÚú˜Õ‡,!ÄÐ1©øi&–xi_VK@ip«Íƒ9¯ÐÞVi%a; Õ¯L?‰0J“*¹’šÅª5ܶ¸UÑ·Š“'Á¬ºx^î²6âV[¥^ à{öeU™ÈÒ|—:0ø=0‡»ÈdÛ«o‡¨ç*J“q%•[­ÆõYÃN¸˜.sQ„L‹udš[2×ð9þIýó:WÁn—ÔÈÿÐÙŽÊm™Xl¥Úƒ¾6×!lNl‡ÙVÙÕ§KU¼¤¤jVã\J%©UߊßB°ŽLcKfáb×ö>a“=Òb›~¹R]aG%[ú÷×js@«/9ðMطݘU×>yɲXÇ@}³ ” ëëF¢´tÜg^‚ÛvO\°žÓ¸wv‚p•ϯz3›K5i¤!$P>”ÄÅ€¹'Ò”VÆ›¬”¢Lž2r´ú@¤UMÃÉKÃúZ¯õ‰¹å6Ö×ÀtwŒë§ŸÂ¦bä„mß1âh|ô|É]}~¹0øÀMjA¢À´Ò(JâŠÝÁ­JP68ÌC&yrÈÌ׉e}­jŽ_cËJ½?êI0¬¯kêÛ>š«W™‹áø Æû‹™¯é|¡B¾Þá."TEXd Ô8”Ä!cwµ*E(ÎJ)ÊåÉ!Î[W"­j_ÔÃáТeX_×ÐXB;¤÷¯o°†O0~?¬:P½Cã (.²í¶±[·Ž‘‘ò!Wq£%ßÔ*leÃÀY)E™<^ˆKåZ¹T•60Ö.ðõ#«µøA\ý¤Á5;RmÆtkdÂ/8§)5~‚¿ ¬^0Ú #åCkg–¦¶eÍÌy)²—±Í¶¿‘ÔºÒ°6Ä¥ª<€(?Æ×&ÉõõuîA„áVŸ’õm0^h.—tÌxR*ô×aô©'ö:,¥H§|èÅ–ªÏ l5z„;8+e¦#b'#|û}2Æw(|Kc–J½ Èl6 뀶¾®wù‹^‚ÕŒo×—iúœ3HÓ êR –ŽÌ”9Š,Y“gP«Ö°:N œ[5SÃöû‰R‡!¢§ä[)•ç]€úœi}`úúºm¬’¸N±4Ð¥¹²ãvÑ`|;f¬(®´Fïlt©„¢LÔ8”Ä÷Z#½Aï–¤O%ÕÀY)N¹U®5YêÑeœ¼d–JÎE3dZذ’þÇ<Èx·ÇñØÉñä¶e •@ùPÚ§ÏþÎFúTR œ•2S¡Â·ßüΦ/uˆZ°~ðšCæ3ÇÔXÊz¼ÍÓU¨žâxõ\2s«ñä¶e •DùD.çÉåfBO&enÝ'iÈåR%™?Fy¸VsS~$uˆ®mœw()Á´r”ºo³0*Dí˜Õi!3½:On[Bµ!sʇBäp>Ý£HTÙ1òè ;ö8M×jnʤ‘Ó¤ï¼äqpÞ 1hò^ˆ<¹Õ<¹Õ<¹ÕÜjžÜjžÜjžÜjžÜjnÕÜßû–qÕ(qpõOkª’Ô}¸ßøI?TY8H«®mhyK¸Ìu5ÍÏÂÎIœt÷eÕnQBÞ—`µRÄÂ`¯·EÀPË ­Ú¦ö˜½¹xû™«ž½>¹>€â‘¡yt¾{?|œ×'j)”ÉÆ€µ}YUÛÏäUùÛÜ{ç@Vå‡/€J1ìF+€¬¿7䀉[OW«O[æù ø¹‘‰y³ÇUY«ª•ˆõ!?BôÈD%D™Wj¼>-Ai6x£z)»ÕÎU R½ùª±’7 dõÙŠ@µg‡ˆëï•\†soØ)œaÏ4ßzfŒ[«W+•±>¹¸« œÿPô>ä |•ÛqLãÑG8vâ¸âêÈ£„˜l´j©µ2ZíÆtÜß+åŒV¥ÔA¬6g<„/ŽæQ ‚H­çSrΣ“ÑçÖd}ØùYqàÔg]€sY]ç;]FëCª@5¼YÓÕ–5ÎC©3å8oÙ)kš1'ûüd6«>T *Ëʆ’§Uz(¥m)ûâ®CD `‡ÖHe/¾.ñ:ç—zN¥È9pgo &NC¦×ƒŒÞ‡¼>¶WÓøÕ°_’ñHj ñ)¤Xe6F„ 7p’m¾-è`'Öc†»Ü.Õ«‹ÂAZ=³þ^Ée8÷ÂF×;<ËûÄJ1{óãŠ+8'€Éª'„Ö‡\Aµ*¿Òø[² ‹ñR$UãY)V¹ óAyɃŒw)ŽEc#<ÕT‡ƒ”»\vW•{R­®«ÉëÉýºtÛn(–ÏzÏ!S×7o ×ï€×Ie®Žî™wõ3]ÔçbÜ—üäÇ8¹5|Æi·Ï æêRÛÚJkʱZ‘RO+ê8£U&µ:]•Z‰ieR‰’¬¢‰(üóJËMŠÞ—7—³«ÒZ@Œ²5Ýa^äº\G˜z™¯sª¾éÏU‚Ò*¥rMÏe³zT¬^Ê:ɬ‚õͦX=>Ü$ bi>³U&X¬Qoybb¹GÄøkøÍ8¯ – ÅÒ˜óýÿn).Õ¤òœÙðoã ¥À^Mmád³ZƒÊóië$s«ªo–oÞê*{»4ììÑeLb¤LÙ³""mx: `:mÉkž[ØgeTˆÑ‡Þ¬)Á„'0*T˜›Bá€{!úîIÞ ‘'·š'·š'·š'·š[Í“[Í“[Í“[Í“[]˜ZˆƒÜj QŠ.e '/¸®y÷vQ¤71ø(Z&†óÒX‘õ?(_œšZ”œÇº”){tÄÚ€m˜ZíÿÀWÑÏ)­«-C“ŠÓò´¶ jqání,Ì‹Ÿ"áIv‹¦½ULØ!h¢™Ù꛿îñ©¯Ýsçk’óAcrN‚ôþ佚ф€…VE4ö0úy˜XÜÒ~å4zʸVã³°%·ñ,é¹ßû)føÃÀqtÃp˜u¦~ã  Þø©ŽÑ*ý“©^æÖ0:åýÏéܲö3ÿ3…ÃÏJÎâOô(¦·ö£›ZB?K™^ Àv]’un ŸlçúÿôWþÀ‚¶i0´p6­ˆ[ì°©àC_5Xý#ú[¿öwX3ábñÎ廫ÄR½{ùÎâ¢NKðAîÏÿŒée S«èÓeª|Ýã¹wñ¢ÇxâºÊÞsôño>ÖP\å„”Ô•6Ò;nVÛm¯fëI$àø‰ÇûVÍ“J-ÛJ%ÖŒ¼Ž0¯óUwûYÐŽÉSõóó×n‘uÿÒmÿè—®Æù«xzµÒË—VŸÆ«ÚIµvnôWÿÚ_ÿqLZØÇòé"_—X®z‡Ã÷Æ 8Ç]Ap—‰ƒˆÍ?†¶CÍ‹Ž‘ž5È4ˆ·3ñŽzw(Ü{7e²*Ȳ`Û°¬!AÔQ“:ñKUnõ•¿Âÿzë]ú1y†V„ø›Ga°úCÿêm0îPY ÙšUx6TT&·hVï9V§ þîßÓ¬žzÑ  1[÷X®z‡ZœËÕî„Ð9ªe¢r›qóJ¸³¸NDß/ù¬¹g·þX¦ë*9o—ðíN6«DÃÃ` Ë{à÷ªIï%ËM´z9—ãTûQŽŸà–ˆþþ7fö\"jþÃ_3ÙþÖç~xBá'€ŸùÜ·ˆˆY›]*KÐŒãî“«%"úÔç5«"ðÈqxq~ü’Æ•=·‘¨j¼´ºSá>j¤Vç·&~]2 xzÀF¸ÕíŸ1X•§_yÞùDÀÎ<#N’ÕîïRB÷Ô}KôÏÿÅ/ói‰Šy†ù¿õË !V^¢ñË¿e²JŸ‰‡}/FkïñAßú7Ÿû· âëS©È×+.–(ec—ˆJ:˜zðƒªóW“ZšŠ°ëª–wïÒÙQ™þáðÅž~aÛÒê„ØÍ„öpç6,e5í¯,¬+¢–”Á,ýûÿð­óñ÷ÿt±võ%O^OøüO}ãן -Oüú7>e²ÚkC¦6£waô_þëC ¢‹|½â›9‘‘×*•šÎ‡ØWÆñ¸Aª)×U¶Jgê8<ýZ€´šx^?„ÿ¾2²u¶­Yýí³õè*^?ûÛÚ‡KC­Z¤[‚ÿ©ÿù0.’–àCµ¯@m¾çÓçß$-ßÄ/~ž|Y¥å[eþwƒeQýŸÙ×¶&cëÊOž4s|‰œc’§JåûŸwsïûXÍ8/ñš¼Î6Ï/ Ú¼;ç'F¯LN^8]ÛeadëZ 1'®Ü°ž÷^†Úü™û¼‡L³‘sBdü%Ó+M¢·`ÝãSKö8פ²÷«ìº*ƒª)gl#Ž3"Ä’gÑŠ˜S Ç㋎©qtcxxƒš|H>–¬Æø=ðŒ:³ÅçýÎmÊjÕå¬ßÿìÕUßòÁóv£qìys©Ü’žLglþC6+[FÍSWg…ö“9õ˜ƒwV3¼1µA ë N”ßD¾<Íû«ËÂ$5eÿ(s„ú¡ ÿ[Ð Û¨bú—³‡žaF.”¨]±K¡îÇIEND®B`‚glyphicons-halflings.png100664023532023421 3077712544604516 30340 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/assets/img‰PNG  IHDRÕŸ‹ÂtEXtSoftwareAdobe ImageReadyqÉe<1¡IDATxÚí}ml\Eº¦W²´^ɺ¶D$|nw'þ ;vÑŽÇ8m0ù˜k¬Q¼¿®·îŸýSû¾§ºúœnŸSõV§;1K½G‰ÛösêÔ©ó>Uo½ÇõTU•1cÆ–YuÖ¼ÅìcÞùa&÷£Â#C,pؚĸ>kÚº Ÿ¶ú–UƒLW -sÆnÇ3Vq¼ë~Nšþ£ïoÖücèÇøI»~LÅ£{ÿà- ÆúñH8%_èïM²Â£wöB¿â²6EW²Õ,Ä¢p†¼‚¬ÅYþ2+­(Y³ë°ð@ ï±&áóA»/š¶¤޵3kXãhàߨ-a荒×AÀÔè<>Pø•Œ'\Á£›JË;(ø}†#›Qz„–ÿ”€‡:4Ãý%m?nf¢ntK*À¯²ælË9JÉØÚ+¡DÞîI§ýYu1Y‘¨¬Z^œ (]YYE­¾f@ìÀâО®lXáîz]‡U×t¥ ¨u¹ ¾&á5-Pš¤íW€}ƒþ@tð‘|Ý#Lõ˜Y’=û´sðŸÈÜ‚¬Æù,w#+¾Rñ”+?‘Æ‹aÖxÑÿ Xé0ø"ea)týGÝ*Ô¡wV®wëV^ªÂrf%xB(qÖ¼¼4>šüWÕG¶#ÝñŽlWU<ÐÌøÑ»XJVѶ·“l÷‹¬Á¾RýÄî$kýDVrÓI¯®£é¤7:ðX%Xà1¾N²ŽEzùºw–Ч;yÒî9ïzÀ9øOÔ%~–ã—~¥áu©ÊÉ—*ú=±ªªÄçIÑx‘cáy}®öY(þ£ëoÖ•u ±N$«^¬jŸþ î®»ÿe\õ‰iXíñœ¬]Ùã;Y-¥r‡ö×ã¬ѲŠ&¾>º!¥zlYÉaVHVNÔ°Ï9=‡‹]Š=‡ý˜°ÇmR¯ÌMÞýŽd»¯OUC ©JUiT}rñW¾¨W'ŸÚ¹uª¸)Ê¢„é÷ËF"YUþ#ðüPã×¾Ÿ„çê&Ü‘íÐ…ÏèÌRåOÀª‹ØwyzÛém$«ª»O²Ñßþs? +^FTÛÁêÝßIEáq÷%¹ó&™›¬‚¨~ç >îMÕÁÇ}]±áÔ–â»wî»A†ï™? [Ëú÷Nteexnî(åæŽª•Ÿ«BºdÏMTÙªpÊ¥šnqÿqîSé?šøØbW–û¼èXmW6ñÎx*{V_ã»øû©!VÃjΧðsã»VL^jï‘Â… XkÏQj©Uôÿ®Ð6œ×ësk”õÌ©n~ý[üqòǸþ-ÿ¦` ØO‘ É:G¦ôÈë”Ý7â‡ßlµƒ"k¸™ýÇãísRùeŠ2ª®vòQÕ=ÎQƼJÏU­X`¡gžQy~ ÄîKƒ·È°Ü°E°]Ôþ#ðüPã»ç:—t¦Ùd¶\T½/uÇñ÷›Úøãç;þسù:¨JËc-%'¦ eçÿ q“Õ ?j¼"/yh†…¬4ެ8¢Zišñ¿ÂùÔÒ1ò|JUÿ§ïuð>¢¾_ÄÿÍNëÌÝ;hxw¿NU ÓJ²QU7\é¹jó©„Ì®bT¢:£¯˜³ÊBá?6ñþ£ëo½J°Â1Ί%ˆóI UY-IƒïÙi4{„=ŸrǤ7ˆª@)HˆKçJ+†f˜4ÏXþ8C×dÃ?'j؃1ôÅü ’•NÑò×<› 3Ú9üŸ’ÕE<Œw߬†€V‡÷z—E}àý^_e檴 p¥çªtë¶¾3úŠ9«,ãªë?ºø±g’lÍY÷OÖÀ¸<öŒÎxªxÕ|†º’aØŽùªUe—…ªðFõ²Óë‡ ó1Ú;“{ EFÓ0`ÿÆãDÅR‘¦ü+’UÖYiDåÙßÑ4ë?ŠY`|B‡¾s2ÈÜyip›Iÿqã»>WÕoý §Vœ¹áTùüGÛßzg#ø %–‹ÏD0#Ü 3žª˜[t¦i™Ø¢º( U×,Ä]Î125Ù|îøNÅÌ­fwØ7w³ æšíÆ ø¿u+¾ÂŠÙÂ]¨Dµb]ßÂK¦ œÙxbWê Õ›7|ã•Ð’ú‡Xã•›•®ü{UÝÖljcù™G•ÏôñX‡k¬þ|‹(ÿh)IUÌa)ÿlp 3ùlúîÔuPUü]D’ð—)©/7~4W‰¸t5å¼J}±ûV¿éù Xõ0«§†zæ ËVM©ÃÆ;>¾GÔ™Õ^è ¯|ø¸gF:Îä£jaZé»ö^)74C#jîwr,еSõlõüGuØ;1éÌvÏm><Ñ)¿}ªò«<½ëðVZ ueÛ DçŸ+jÕy‡–믫J6V{j ý±’K­>´†Z÷ÕáQÕ–ïý·&þmZ:ž–¾1ïU¡MBå~¢åò ÆaíÀƒ:ä/᜗:KÿWžWOÒ &…­ŸªºY´ª®2fìùñ7cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘±åg¥È*3fìF5ÝLb´‘¥NŽÁ2#Tf=Cé`!–üZG¯Ue꣇‡e‚Å2VéÏ<œ1mkS¤ÉåÅ4iÏ—*.š{Nü8X™aj~ŽéïÚ€ânAšx,ÿ—×%fE:Ã|éY˜DV÷j žë¬Â¢Úlg6Â(:«k~Œ¥÷MöMâ×5?¦4 ]WO·>ºøè¯‹W¥È£—Z”iGü|˜QËGüJe´çK[YcÕµ¹pmjE\f/¾ÇŽ8&¼OQÿ3Í ‹âœöý.3t¹êt2'½-Và8ßùá…pƒXµS°rÇY#J!«ûñQð¸ê ƒ"¤,ub×@F­³úKß:¾uÜ^íùiy©ª[]<.Cw¯‹ƒ¯+W\)¿ïb¼ª k¯r-¡åƒî.M¢t¤Ú€ÔM¨‘qÆÊ„õÀ²þÓÛ°§·¦#$^X$žÑ"œ§‰½»ñV`•TŠ4™máÒ~÷w%Pµp1·š|…+&UxÔY‰Š8«Å*Âr²8:£ìæ¬ôàk7QЃҀTªºëâ“Êú¸ð“Ö$ˆÛÐŽõÎÆ™¿ S>~§Sü®õµ„jÌsá­:5Þq.w°&_Z.ôX=•ª‰ü:Þˆbwë`”ÁÒ _àkd‚{'¢Ê0Û:ØdÇÜsÔþ#ð«q«¾i!224Óö•ünq‰\’9„-‘ªKUTäsSU½ìuVo•@;¼U®³zÛ>^ú¡=¼—N²Ûö•ªîúp¼¼>oõ…PÁ¿O…ÇÚ  @Iûêú@à£×'G“j5©o¯*U®>îä^«*‘e›w‹ä>ͫʧ‡×á« Q°†”5 Í„¯Êç†<$ç#ž5¶JÒÙ»ñ¨jÒö6eî)ðÛ_ ýîùûd]±Ó2‚°–B:À‡ª^ò(*²:8JÊÁÑYÕS鬆®òÒïKÝ—“è ]U4_Ærj¤{Üõá5’ׇãaÇ‘/íyó—Vÿ?“ìG¿tÿäG¿¬Ôõ‰b@xPUŸä7O3§|ªéª ÿI°òQ5ÎÝQü³GwÄ *(;³w¢f·0*×PðUU<Y¤Æ”ˆÿñ¶v†²¼þb¡ÎtÂö5{2!ë,}©Š½ìÒ¦–”:)·Îj2Ok™Îª’' ÖŠ0I.q\(Ä%ojQ˦âÄ–Õ‡ãa<ê¶ëÔ‹”exÜAgt‹û'£[d;׸Ûù«ªú`rÈcd»ìž¦–j ŠP¹FUÝ$”UeJ½I6ÙTü³É&Z}ªªîz¿Þõ(•z÷ vfu¨zÏ ¿›{}Û¿ßÑåÝžlx“ÎUõZï謊Ç.×Y岟bÌÎç%»†Ž—¨nwÀ·@×Ç©¿¸S9ÞÖ|źs%Ú>Ë_õo#§üž9Ø\¤EU~ÿ/ÝÚ÷t(r[½QµªZu™ûOo;°ŸËèÛ!MrU»]ÛÎ0TÓêcpDÅ‘Õ?.¬õÚcÎêPuí±Íç¬F€²€™û²;œ¼ªŠL_¹©ª«³ŠùSÞÓb}¸R/ŒJ_Ëç+ßÔh¥2$õaµ£i ­UÕÇ©¿¸S9>¿Ð„}7ª6rÊ©µzuùÞíùà~国4­ýoĨ 1Jœ­€Ñ µŒ^±Ì˜ãÀ~££iCÞ¸5Àÿ5G×¹]úg…þõwö¼sùn zTuO=Š?/“ÓÛýzƲc>ÚþΟb¶#7Ö»cg’æk¿þÞ›TU—j÷©*-T=]Ýúèãuu}¯£>ݨÚNЭ Ùø[ ]â”:%/_› ®ëS÷z]6D.Ôm®‡ƒ¬ÄìD7UÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cæú£ì´>ÞJ4hëPP…®+«§A›;'G_®XK¡ÚmL«…˜5I.},wFFu§Þm$S–-¥E-;Õ C3I-`ÏBëßRÕx1¾ˆÒ“T±JÝ•;hΊœ8£ DêYøªJ‚o;´çÀþÞÙYš5ŒM²ÍçäK·¬É°Mµ;šª¾%Pº”Žd9Kîh”ø€nç´D[z¯ÅgVhè,®¥'Cº p!^MÖ(³WK2ªX¥>UQ‚Ä%âö°^ƒ›p8 ˽Ö^#Ζ؄+.@—ÔñgêCêz%É”-Pr –‚¸K¿Xÿˆ ¼…‚¦nÓÁ·>½»×=‹Õ”ÞѨðÅeSvR—×£’L—zªò½Ö5%9UQS ¢¡\ŒW‘Õ«ÙÊKÕö'Üh™p)ô J·”r˜h¡ß žîM0ÊF ¢Î(f_¤R5ñ/É//§Gïä+óª‹õþ£±¶x 1"è¿þ³¨eSðú 5… ­Ö:Tõöf¾ð=+Š7ÉQɧ§\Œ—“µ²TE¢¡ §sü༬ïrÆßÞY®s8ú•&†kíìùû¶ÿ­ª#pSÕŠ5Mà³Tªb«ªDÜŠ[Ng¡5QÒ\sÇí5PB@[ñ8ɨ‘V1ªª¢¤¯¬&ö¦4Wsy[šÇ¾ ŸwÒU€½ñ2²V–ª¶îì7ÁÂ7”€jïÄÔÐÔÐÞ‰d^~Yf·íC©«_‚Àh;a.ÌÇê&†M® i §ÂU–ªººªWpzs`>÷/Ö"¤Ô'OòIöáÚÛ²ç‹y‰Š›€¬:ÏBzdõÃû“µòTôŸq£ï=Ð¹Š­b:…‚°"ü½ºùm«/¾Ñ-/PÜ÷WÁDQóÇ´Íâÿ5Éîþ7Ÿªºúªúmå`ôHý¥%AÚóVµÒ!ÉH¼Ô›×¿¥µ÷@"Q£¶zãÅÞ‹|ëß’TÖ¿þ-ï³*OUý^ºûÒ¦6üº©­û!Èî‹Cw„kÑ|h¢&Hd5ŽLEYày‘Ž'ÙÆ£7Ÿª%½*Ó{èü=)­°ÆZ¯%Ù¿§P »Œú*GÕæÜ]ëÀ•È/—¸8LÔÍwº$?8üñM¹)\į«ä£µ/#ª7U‡fd7'6´\h1ï vI’f”EIr˜˜Ç=½1øwöÜ\òWKÖžVZÄHKüœgþZ‰ƒÍ¡$m¤äx·‹ÉÒ %œË Ý`j}ÔTuTöQJZ¯è*H>*QÝxkLFT¿ÀïÞy†“UÕÅ¡-†)‘ôb¾®iA¨Ùó|q`Á¿F­'ÁŸÔ+ ˜áç4^Q¶y ´x‹ÎH)’î#Ät^­•?@]^`AÕRêSŠªq¨jgžB:ÅrK÷Û·l<2ï-4¿YŠçKâhgQ°®öLÓÄxœV§wÞP¶ì~ÑÉM õΦ¾‘¸0l 3šÆ…ÖaÅŠITˆÈ€hwJ¬m¼¨¬èÁ¨­™xIM¤Õ¹¥¸|«U7xˆS½³~2ß•?ŠkW1k¥ü½C3]Ùã;Y­•nSÞ—ºÒ‘AíeºXÊYzø8,'‚x‰< k7Kx‘ƒÞÂ]Öù$³¥x¾$Žvë´g˜T#wû“;oðÉýòÂ@ Èz¡_VÐÚ÷mán|¢HÖµïËhåØZg-^TAn«¯-‡ )ƒà–¯@4[*Ô9xKãÅÆ‹çÖÁ²²j>Å!,¬VtË:eù°Ô—Âqn8%oh²ÊéS„(2è\Q‡Ë^éaigéáñí¸F—¸3€³v³TUDV¸l’QÈê…§¬W¥cÝí%’UàåªeŽq©4ŸÒº/í¾U¾ ¥$†_æŸQ!Âä´>¼µøÉÕt×|é ×,È›G<tßC÷¥Á[ÔxTXmf|ì·<©ÄOÚ¡¯MTÙ|(w:ÌÜã_XÚ¤Óþj7w°´³tñ¼å » AXßͦªpÙ$‰^xZ«R±îö¤‘ÕÁëj¬x”îæÖ`ð3=â^±ñll±+Ë—e»QâÙ8g8V ‡‘+þ9MËïë/£ªÀž–oè14snb› ¡ëtXÙÜÛsŠýÉü×vEÀl+@\Œ’e¸,å,·cÑ®â<§(†ÂiõHVYªr—¥Qà O7£aÝíI©‘>Q%dÕ#jUÕ†³|; H¯Ø[bÈÂÁî¹Î¬Ç#ÊÊÆá¡«ó,Wés7NT1~ÝÔém&Ç»²{' \­–㟾· ’bíBKJ¾o8å%·!˜…€$•ªQ—›¨Ïj:©‡/¦RX)$SyªÞ³ ä§ëR°…DUg_Dûƒè»¦ÆJÏ\Ñý굪jßN®¨Ö–SU;~·?¶÷O‰¼h˜ssýdáõÆ£}“6Î(T <ÍÕ_ž4ÉïÜb5øõÔÖ £^N ªöøNŒ%8QejF¼7toç¤îMyÛÓ©ê`)g[§‡/¦ÒÐÌÇ÷Ü|ªºïÁ?òÆÓ©JÁæåuÜG§ºýãLï啎Þ/=£CTÜ hdÊifHºÔøcÇžùæÙâG4öŒ,¼îøóáõ޲`’DÕžÃ{'xý£ÊG_p/5øâ‰@m +×$jVÔH¨¼Š3Õa"…*Å©,å,¡ûH¿JÒµ¯ȸ…T^Qyª¢o&IÉžJUVwW§ýÃLçeM¸ø~ñ×3t¢¢½“²»A©ã6ôÎÚÁrÌÝwɤÊ6œ²Õ¿í¿ Ðò\0HÀL%LªX5ê’c¨¡Æç“@ HHÃZ‹º|NVÜË+7WM«Ã{ü­¦¨cigéâ…à*¼Îê¤È¸UšªÜ7iÉž«Ð±zŠˆúdõ *Ì ?¯gtäÇXõ‘í8îõÌO©°XÍî°:ïô]2üÉ]Ùp^°¸++òÇ>Áø¸A”èÛVÚ›Eí{âæá “´DB.È&¨/ÅÒÎÒÃã56Ÿƒ©AãrxY#Ü•÷yá)ô£cKQtȪó²¦à~­Îð¶ÿ­êõŒÇÅ! ì™;¶C}ʃîÈtf{æ6Ÿƒ$N§üVsj þ¹wup˜Z)zÅý|ú-”wìg+nÞMVjÆ/d+U½°”³Ê~ͯÄÛêŲi—ó:_ixš£ wµêhqŸƒr>©é§ƒ-Íx«ë¼¬)ºßÝ·ÚyÐÆR=! ì–ÓÈì:øŠJ/lÌÊIk‡ÔÃV@¹nåÝ7ï475ãç8«ZõØ KÑJæ(þœUxšzñ1w)^ê \ŸÔ£žözȪ󲦨c‚¨©2fÌØèvª+‡6f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘±2›®Nï÷ oŸC–\žõ¯¸ F1Ö´Ø ¨UZ÷JVÌš\®4ÍéôÆM§÷û†gq1z{&³YúT £,ðHX~D­ uÁ\Ágô÷}xþ>½+Y±ÀèdNÞÌ®†ïo‘l ¸Z´X+F¥Ó[ž/jÃÿ+S~2/jVîò¹8óJ»r^­Ûïè³Ô‰]J}JÅï é*Û<£í2Ô°&£JÝ£ë¥jOµöM@âѯ#0àà¥Oß[‰úS‰Ê÷·°‡†XÓB^ uzùe…\§×Á»]…ïýd®ßüd.°¥—/„ ÔxXE ¢f'vÑëO»_´¦“‹HŠ${è%;•kçt7Þßm¼ƒÅ‘|òúd{a¡Þе^£Ô…Ç›¢ÚŽE§†5Ê‹¦ÑBr]W·ú¬=¨_¹šÑÃSA¼»†f(ø0 …ÙoU×5Çq ,ë_\×lÿuz˪uz¼û㻲¸ö×oâš=Yiå÷Î~|§ˆ 0+¬=VŒÕòµö”ò…¨J—ت ³Ý/²›Þ½ÔzMÖÐ\÷zCåLÃÿ±Ã[Uõ:|k*^8"¶\WÙš\ .¨ïXTjX5 ÎSk½F‹u\”1çƒ ­ž—q'Žöm¿Ä£/ñQ¿”UØ•£*«AɽDNZ×®?_¨[#Œ Ë4þ:½^j|Â5LÊGù­ ||©­ÂøÂBW{6[uQF©¿¸µ.1Š´$qFÈí9´Ç‡IHg)\÷˜ÕÍàž5°ê>CÃ#ÇÃuöXÜZ¡é$Ÿ#*<Èß«sRÉv¹1Tj>J¸ºm>*§«ãÆ#‰¢(®é Éé[F¦hÄsשõ5õÕ*jQʼ–&š†è&×ý&Pêˆã»çŠ›L®Ó[ÞQ”áª1*üÔ«‡ ¯š;¤Ÿ´X}ÓI•ΰÀ[Q„?Þq¨Q½Z Hèóûä÷Ý™•ÕñÖžV¶ŠEsB’¼CÔZ9¾éJTK¶Ë «¢—ÛtuŠp¡«Ë·ºê /©Oïì±´,.k¡Ud‰sƒOHMg4=-)ï+Ø¿èý“h2Á¹N¯ƒw÷/r|WÖQn=·GIUù;“˜'ª„j,¬ÔvðîfñdzþÏí™p¿e¬¨ú$õàºÇV GTYñsBZÄO‡1pçj:ü˜›¨rü±Ž"n©TUSCgœœr Çve§†¤AìÛ˜±™Â˜FèC+Õ–#‹[J–¸òTeý'v9-Ô3 Dîm¨Ó»Ôuýuz¼û㶘?ÎÝ0»• ”oËÿ÷ ¢ÒêãhÕxð«u‰YõÐ &“·î§‹»_”54×=fµÐÀ07‡ÿkלUÕð0‡·]D:Ôøæ‚ä‡Ïj±dwÐ/+žÚP¨¡GUVóäSŽç<ù”\2¨u¦»at´cÕ^zYÜR²Ä…mCÔ+ÔÒ7´¼þ¼#‹ã,|ú:½Åi™N¯ƒw÷º*|^sàÜmå|”X>Ъ^ýÝ1\—#ÁÓ͹” &˜¤%§{,2À¸Uˆª>ˆÝŽ.c05zØ#ª oëg…öNà£×O+©©QÊì“­’ï ‚®•¢óÅ,ÎÌË—Å-%K\ù´Òî[S_`ýyŠ«+›úb÷­¹_9Šë4ÞÒð"£U©ò+‡çÎŽap’}²I­¸èè[óM,Bª.ÞNtñœàÇwªH—Ûj”漬…ü¬E‰ ºïœú…žLÒÍ߀ 0DX(©kÚµ§¶¸” …NoU§÷{†gquz RèwkÕ§Rx'ÖuZà[šóÙÞ3'Ÿ¥zÝyy²¸×“%®ûsŸ½¦ÿ Æ•úàü3jYF\ÙÚs¸â÷=m1ª&ÔúŒVÙ•ÂAɼ?k\+]Æ6yïモ¾»1ŠÃêgtÝOIW7öaïl|1«ì­É è©õÎÆ>$]e 7ÈÚØ¿W¯I¼e?Þ¦ÑL#•àÿ>|Çÿ½ó íÒ­±´]ÚÒ p±M5M­UdIú² 61íýÔ ûeǼY½Gððþî»híO n‡Ô3çÕ­R:^×k_'Yuuq#ìáçpõ# âJ¿‡Ÿ‹¨2Öx¥™ñël>×ÿOjª®ÙÌõØcY²é¦ƒ¬!•Ú¡+ðsZ/üíìÞ× D‘}Ïá2ž AƒY m™”®ˆp®c#Æ<'x»ÏSKx–Ÿ`å¦*W[,e|‘6â·BÿH)㶤kj•ÈÙp™»D¾U(2qzx‡¨9éÕÒÉ*tqa™/,ê Z[¿¯ 0Ï>ýìÓ¨§ÖœóŒ…xN)fïăûÃ@qÕ¨‡ÍÑþFUÕ¿—w(·¸a;Ë‹ð>Ò|T†c|Ôw2 Æ÷µeiT]*æ!_\•WG{ §è]¬^ÐÛÝ…ßÉZ5ÌÔí¸t|‘6ÎoYHÈÎÈëßa齿¬ÛþþO@¿=í«„¸Ä my^ak俺¸ñEü.¾¨ÒÅuô‹îzà®]#Ù¥¹ hWvü(ëî:Ô,•ã6ÉõA“ë߉JçÂïFaœâÞ\ â‚w©øW×ô¦ex>v¡<çë?|ý‡¶Ô&i_¾q†zë Ïð”É] e²R_Ä7©|& c*kÖ€4fÔñÈ,J «U—øÜ_Äh“ø\1A¤„îîèù•¿ùµ¥÷ºu\·ý-‰L\Ïœ^çê~ÔP¹hrÃÉ*tqa0¯ƒfTêâ:ßMUùÅ;qÔ>µet‹u¹MÙÏYÃÖA>†Ž€á«þ).,ù ;ɦïCÚbwÛjE)øÔW°â¾ÅÏ …ÆFÓ«@þs4¢e¢6^äµQ9oI}4Èx<û“Õ.ÕB?¢–BÕÂß«ë#ˆš$‘åHxì.x9,•Ìa!®RTèpgd5ºÿ‰¥÷úxB·ýeŠŽü².L7@¥*ïž AsduttSëâVUaà‹RU|–I xG¶߃$T®üÃëýñ­Ÿ¬›Âë#_œùIF÷MÂŒö_Xõ@fõo÷ÙÂQ°IDÍúI²¾I?|÷%Ž¿÷¥$Ûrê ¥ž¹{œŸ‰ôÌEÔÛNĸÜwÞ•ó²qqé?ÕíéâDؽ}­}o•/`Ó£ªCT±i ¬ñÈ/§ywãOº §rD 9©ï—YUD]êà˜ Î‰Ãú¤¢@sÊåè]ÙÁ¿+'UaL}Ûû hƒšrÌU±Ýßç'7º:ººsU|k)Hú€@§ôáˆñhßN¾qž#è›ÏµÁ8Œïy­Ë­àX²ÇÏű#ö¶wýÙ ´1!•í‰ÆR'7âçfÈuãד»²0Šù¾¿Ýp¤!WˆºÃ–W+NmÙp¶\£ª š-‡ioD$Ï£ãògúÙ Ë…%%ï¼ÃømÊíV€]”̱œ™ráw*²ÊZÞ}¿ùy›+L» N¾äoçu©jÚ}Îxt‡ô•“¢ƒ)lSŸïtuqõ¨Úx±ûÒém NyKÌUª OnDbƒhf}ëk¼>Î6ÞuÀfTï%þøÃÀ{‡ö¦ <ñ®¸’úÛmj¼¤FÍcÂmU¾ï«ì¡ùcßò;°wŠí8ž‚@dG¹FUAµü&©‚ ÉçêÑË=nðqÕ5]iP¼ˆù}§zê:¹kâ¼¶‘Ý-¼ÄÊ“» Κl*'U›zãax«W…µÔFÖdZº¸zTí¾NR“s+’ˆ#«ùø wÎÀzgi:·°MB„Àqø¿žtúMÐ ül#¨^Ö'G™ß£Œ*^ÚtÛ{œØÕ=§rEý¹êR“ºnQù$adJlÄ02%Ÿ®TÚŠ^Š„ÔÝ<Ó~g§?ÖOóf*U×^óÆ?üœ:¬½N¢Šž”®+›o¶[àP¾UÒs’|çQ“»R']çVó-L)H K¿äž mYËÕnµ\±¿4}Y©ëVÓD½ ÎhÉÍøRð©ºê;g¢Õ-©û'…3aסæM¨ D­h£}´1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒ3f̘1cÆŒýÿk¸*Èš4í`Læ$bÙ ªòœÖUêì²ë4\dtœ×Ö'žÿ®>…HÈ„|Õ.–Ü+Y+/‡GçyšÎó2ÅOCWvþÓò¹3v,«'kiaåÏÊØÈ ­ô¥W³¿‰Œ O6߯Eô=Hv $L°l¹xI¾èº/†˜}Š^]èÚøÑÓxŒ™\3Ôè æÉ®5¯ç °¤ÿQÑT&Gç9AÔy^Žø¶¯úÿiä•}OÜó[5Þ±Æwq¥4,s JJ×ÖI.myEÔ^ë%ˆÍ–'VñB~½dׯ‰Î}ã*þj÷­*‡ ~™ÈuÁTå®k†õ¡\f†KЬÌ*¶®Y]›ÀóÙ_v'IÙÖ˨æºáëöé‘©6ÒXš—oÿç'Ÿj&uÍìɧnÿgúèT]oíò„ÚŒþÆ9”«©ó\*¾wVHÓ–¿|Ì Å>Œþ:ò5EFø'J ¿‰ËýÉ`î¶Ä©!ëòA¦¯˜ØÃê® É ÄÙçe~ÀÇ_;ªÌð5çױϊ©ûŒà¸é•‹m_&þO÷¿Æ«ViÚÅÝë<}"«éºð°hW9ÖX±6öÐKPÆ£ G…"ëÆ­£?¹ó/Á÷”åO¯^Ø÷ëhC¦HáØÐLû¬cß«¬iÂÌP¥ªjßãÿ)}ûûÐQQÕ¦‰#tM§g¾°9ÿ ÆÔxGw—‚ïÊÙ~d;_åJ+ßRỲ¸<üõë;¶e¢ŒÛ ÞÂ5/Qs¥/5º²N[òÉ!´a+„N‰PÏb+üѺ’éIÆ}´ûÒà-ƒ·t_âq“U=–MKÜÊžÙYÇÜþ5nožá*ÊÇÚîv—Þv·bŽÊŠ{]¸â¤|” ~ Z¨{-èÿˆú¾¸ë‡^ˆý›ÿFVviϵ3™YaÙÒü˜ô«=6nŒ­ødS;œ-ŸÍ´^;°uꪪ^ þ|ð¿=úó_Èw+°ú"‹ŽøÊàiÝ&4¡ól“#´w‚“iíûr|W–ô‡œ3Uø$Ûý"Jì~‘ŠçO@]~tÎâRJV³©MH¿w:̦“¼ìÕ@?ÿþ>ëO¥Ëé?èvdrº²tS§*$í&~1>‰ÚÀ¨«äÖÝZ}^ùnÍL(ôÆ]Âf*ª&ž*èQž’aßIÊÇðÐê„|”Œ3Ô*°—‹÷O¢ê?ª¬úç‘Áÿrÿ?¶*ñ¶³4ˆGyz[äk/täk—‚Q–ÏÏ–µÏöWC¿CÙKºk/Ýúx§¼5×|ùäSÎ*`’ÏϹγ˜Q±ÜÂâ¦E¾w”›œy’ o ‘ØK«YqTøbªúãß$ÙÎóÏ-/PtªsZNî›KÎÛQ¾¹*>§‰Ý¢ì‰ÝåU™@»Ð"JQ;¢ÄϹ&¼ ŠLxÒ;+Tã /+¿ôÚOÝ赟ÊÃ>è» (T²›þ?Ä·D^N*©'Âpöï·öþ$I¤¶õWÖõÔW~Ó =üæJ|ÐÔ_‰óUTeÁÄ7Ö°P`ÿ;CYjkà=ßsßU[«åmß™-¼úÆ;°};³2|÷¥w¶€o«1ƒp‘0‚Æ~>†ð0Ûßõm·õ @JïrÇŸócÙ·4¢ÍœØâ?qÅ÷\ÕUUéIV?2…åL©‚/Æ+Шꄾ< ½Ü‡^TõÛÛ ¥?t©j\ÐJr£¸ÜҀܧ˜B*Á˜³þŽ=kÿmü¨ ¿þXï,n}a¿Þ©¬Õ’¹Iaù¡dìp×·öãlñl{\ïú6v8”¤Rµªê…Ÿº·­²Ò²™ä¹f¥1¹ÝF|ÕÂ;äeó=\DÂß ,D¥:ψ¢rñ˜xQTâ—Ž’*|{n¶‰S 9~à=ü}Ó•¾ÉG~%jû:D…©j­<Óáž«:£ªjO%Œþð ï$T8!jÿìÓÛvmÛå|'O¤‰Ð—‡Á¹➱z\vsIv`ÿÈ”¨Ê¨jµ-¡^›$-€Î^¬ èG Q¯¡{ÛmŸ¥¾`ÒTûš#Ûc‹Öž­ã¸Ÿ|nâ.ߪNŽ$Þ÷O ÇÖÄûÉ«šJUV¿áìʼ›t,ßéÝýâjÆg¶-÷…þûmï­ðNV¶£ó „zÌþ:ÏŸÀ—(ÓΙ*|1UÕx=Y±Ök*üƒûùt« ÎM³þÏ÷NÈåNÝDUÏhK™«ò ØžX(刄Rvñ!±#B_—«c¿xï€R§·ʉŒªo¶E5Dg>?úf›¤©XQ‡¨QéË”|@‹"ÓÕ¡M‘—£ìÈveC×>ôÒm×O$HãÞ#]Y”µ ®I=ÜÁŽ)_Àëë`©‡ƒkø©¦* ’:aØ>!X³çË!›ªWÖ^Ýô†wÒ’œ¶l'è<;øvwgIÀÓt¤_Û?Jh”Ÿ`ƒ#E:fdx=¢–6Wu<ƒ†ÒÒöÌÛÓ‹âd2¦di¾×ŸË‚Œc#h¬c4íßûÔ?<´çÐHº¤à»FYo´ûVp¿Nñž;„Ý·J\ûÀ ö¶‰Ïÿ>„` (Šˆét¸3{Û>⦊Áã;;q†ºF±‰‡xÝ4Yc«ã‰µS¿$w¿. ½……—dÝÐa*k£«ó|£ðQÕ,¿í+xñ’¹ýs^¨Kß«ÒæªüP^Üéîn€OÖ®L5m­IÛwl?-.ʲÓÛÒ÷J8 §FÔÃê…Õ ýB.-:2…ÀÈ”Ý!ÐÎêÌ/AÊ#bÕÛ_m%ÌIÄ(œ”â$|¼PZ[¯÷¥ 1½G¬{^ç#µ®òòÄo>×3×õmèw?'Çcx¬£·[ü^•:W•k/—`'=ÿ°Ð~Ö¥ÆêúWÒ(õgQ¶ªbfv7UŽzÉèMþ3µ‘©Æ+Ø‘Kç:ÏÑ4|GñCtªùAË+Kôÿʨú{@©ÊÐÆ– ±[0ø5íðý±ÿE›|yn4MIEND®B`‚class-browser.html100664023532023421 541312544604516 25051 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser Schema/Class Browser

    [% PROCESS partials/path_treeview.html tree = classnames %]
    [% PROCESS partials/path_treeview.html tree = inheritance %]
    [% PROCESS partials/path_treeview.html tree = paths %]
class-detail.html100664023532023421 657712544604516 24644 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser

[% meta.class_name %]

[% IF meta.table_name %] table [% meta.table_name %] [% END %]
    [% PROCESS partials/class_inheritance_tree.html tree = class_inheritance_tree %]

Class Metadata

[% FOREACH prop IN [ 'doc', 'is_abstract', 'is_final', 'is_singleton', 'data_source', 'table_name' ] %]
[% prop %]
[% IF meta.item(prop).defined %] [% meta.item(prop) %] [% ELSE %] undef [% END %]
[% END %]

Class Properties

[% FOREACH prop_meta IN property_metas %] [% SET row_classes = [ ] %] [% IF prop_meta.class_name != meta.class_name %] [% row_classes.push('inherited') %] [% END %] [% IF prop_meta.is_id %] [% row_classes.push('success') %] [% END %] [% FOREACH prop IN ['data_type', 'column_name', 'class_name','doc' ] %] [% END %] [% END %]
Name Type Column Class Doc
[% prop_meta.property_name %] [% IF (prop == 'class_name') && (prop_meta.class_name != meta.class_name) %] [% prop_meta.item(prop) %] [% ELSE %] [% prop_meta.item(prop) %] [% END %]
[% PROCESS partials/class_method_table.html title = 'Public Methods', methods = public_methods %]
[% PROCESS partials/class_method_table.html title = 'Private Methods', methods = private_methods %]
class_inheritance_tree.html100664023532023421 50512544604516 30554 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/partials
  • [% tree.name %] [% IF tree.has_children %]
      [% FOREACH child IN tree.children %] [% PROCESS partials/class_inheritance_tree.html tree = child %] [% END %]
    [% END %]
  • class_method_table.html100664023532023421 251712544604516 27720 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/partials [% FOREACH method IN methods %] [% END %]
    [% title %]
    Method From Source
    [% IF method.line.defined %] [% method.method %] [% ELSE %] [% method.method %] [% END %]
      [% FOREACH superclass IN method.overrides %]
    • [% IF (superclass == meta.class_name) || (superclass == 'UR::ModuleBase') %] [% superclass %] [% ELSE %] [% superclass %] [% END %]
    • [% END %]
    path_treeview.html100664023532023421 115112544604516 26743 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/partials[% IF tree.data %]
  •  [% tree.name %]
  • [% END %] [% IF tree.has_children %]
    • [% FOREACH child IN tree.children %] [% PROCESS partials/path_treeview.html tree = child %] [% END %]
  • [% END %] property_metadata_list.html100664023532023421 216012544604516 30655 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser/partials [% BLOCK render_value %] [% IF val.defined %] [% val %] [% ELSE %] undef [% END %] [% END %] render-perl-module.html100664023532023421 54112544604516 25742 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser [% module_name %]
    [% SET lineno = 0 %] [% FOREACH line IN lines %] [% lineno = lineno + 1 %]
    [% line %] 
    [% END %]
    search_results.html100664023532023421 43612544604516 25271 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Sys/ClassBrowser

    Classes matching [% search %]

    [% IF classes.size %]
    [% ELSE %] No classes matching [% search %] [% END %] Test.pm100664023532023421 52712544604516 17444 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command # The diff command delegates to sub-commands under the adjoining directory. package UR::Namespace::Command::Test; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => 'tools for testing and debugging', ); 1; Callcount.pm100664023532023421 1532612544604516 21433 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::Callcount; use warnings; use strict; use IO::File; use File::Find; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has => [ 'sort' => { is => 'String', valid_values => ['count', 'sub'], default_value => 'count', doc => 'The output file should be sorted by "count" (sub call counts) or "sub" (sub names)' }, ], has_optional => [ input => { is => 'ARRAY', doc => 'list of input file pathnames' }, output => { is => 'String', doc => 'pathname of the output file' }, bare_args => { is_many => 1, shell_args_position => 1 } ], ); sub help_brief { "Collect the data from a prior 'ur test run --callcount' run into a single output file" } sub help_synopsis { return <input; if ($inputs and ref($inputs) eq 'ARRAY') { @input = @$inputs; } elsif ($inputs and $inputs =~ m/,/) { @input = split(',',$inputs); } elsif (!$inputs) { @input = $self->bare_args; @input = ('.') unless @input; # when no inputs at all are given, start with '.' } else { $self->error_message("Couldn't determine input files and directories"); return; } # Now, flatten out everything in @input by searching in directories # for *.callcount files my(@directories, %input_files); foreach (@input) { if (-d $_) { push @directories, $_; } else { $input_files{$_} = 1; } } if (@directories) { my $wanted = sub { if ($File::Find::name =~ m/.callcount$/) { $input_files{$File::Find::name} = 1; } }; File::Find::find($wanted, @directories); } my $out_fh; if ($self->output and $self->output eq '-') { $out_fh = \*STDOUT; } elsif ($self->output) { my $output = $self->output; $out_fh = IO::File->new($output, 'w'); unless ($out_fh) { $self->error_message("Can't open $output for writing: $!"); return undef; } } my %data; foreach my $input_file ( keys %input_files ) { my $in_fh = IO::File->new($input_file); unless ($in_fh) { $self->error_message("Can't open $input_file for reading: $!"); next; } while(<$in_fh>) { chomp; my($count, $subname, $subloc, $callers) = split(/\t/, $_, 4); $callers ||= ''; my %callers; foreach my $caller ( split(/\t/, $callers ) ) { $callers{$caller} = 1; } if (exists $data{$subname}) { $data{$subname}->[0] += $count; foreach my $caller ( keys %callers ) { $data{$subname}->[3]->{$caller} = 1; } } else { $data{$subname} = [ $count, $subname, $subloc, \%callers]; } } $in_fh->close(); } my @order; if ($self->sort eq 'count') { @order = sort { $a->[0] <=> $b->[0] } values %data; } elsif ($self->sort eq 'sub' or $self->sort eq 'subs') { @order = sort { $a->[1] cmp $b->[1] } values %data; } if ($out_fh) { foreach ( @order ) { my $callers = join("\t", keys %{$_->[3]}); # convert the callers back into a \t sep string $out_fh->print(join("\t",@{$_}[0..2], $callers), "\n"); } $out_fh->close(); } return \@order; } 1; =pod =head1 NAME B - collect callcount data from running tests into one file =head1 SYNOPSIS # run tests in a given namespace cd my_sandbox/TheApp ur test run --recurse --callcount ur test callcount --output all_tests.callcount =head1 DESCRIPTION Callcount data can be used to find unused subroutines in your code. When the test suite is run with the C option, then for each *.t file run by the test suite, a corresponding *.callcount file is created containing information about how often all the defined subroutines were called. The callcount file is a plain text file with three columns: =over 4 =item 1. The number of times this subroutine was called =item 2. The name of the subroutine =item 3. Where in the code this subroutine is defined =back After a test suite run with sufficient coverage, subroutines with 0 calls are candidates for removal, and subs with high call counts are candidates for optimization. =head1 OPTIONS =over 4 =item --input Name the *.callcount input file(s). When run from the command line, it accepts a list of files separated by ','s. Input files can also be given as plain, unnamed command line arguments (C). When run as a command module within another program, the C) property can be an arrayref of pathanmes. After inputs are determined, any directories given are expanded by searching them recursively for files ending in .callcount with L. If no inputs in any form are given, then it defaults to '.', the current directory, which means all *.callcount files under the current directory are used. =item --output The pathname to write the collected data to. The user may use '-' to print the results to STDOUT. =item --sort How the collected results should be sorted before being reported. The default is 'count', which sorts incrementally by call count (the first column). 'sub' performs a string sort by subroutine name (column 2). =back =head1 execute() The C method returns an arrayref of data sorted in the appropriate way. Each element is itself an arrayref of three items: count, sub name, and sub location. =cut List.pm100664023532023421 347512544604516 22330 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Test/Callcountpackage UR::Namespace::Command::Test::Callcount::List; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; # Transient class that represents the file as a datasource our $TheFile = '/dev/null'; # This will be filled in during create() below UR::DataSource::FileMux->create( id => 'Test::Callcount::List::DataSource', column_order => ['count','subname','subloc','callers'], delimiter => "\t", file_resolver => sub { return $TheFile }, required_for_get => [], constant_values => [], ); #class Test::Callcount::List::DataSource { # is => 'UR::DataSource::File', # column_order => ['count','subname','subloc','callers'], # delimiter => "\t", #}; # Transient class that represents the data in the callcount files class Test::Callcount::List::Items { id_by => 'subname', has => [ count => { is => 'Integer' }, subname => { is => 'String' }, subloc => { is => 'String' }, callers => { is => 'String' }, ], data_source => 'Test::Callcount::List::DataSource', }; # Class for this command class UR::Namespace::Command::Test::Callcount::List { is => 'UR::Object::Command::List', has => [ file => { is => 'String', doc => 'Specify the .callcount file', default_value => '/dev/null' }, subject_class_name => { is_constant => 1, value => 'Test::Callcount::List::Items' }, show => { default_value => 'count,subname,subloc,callers' }, # filter => { default_value => '' }, ], doc => 'Filter and list Callcount items', }; sub _resolve_boolexpr { my $self = shift; my $filename = $self->file; unless (-r $filename ) { $self->error_message("File $filename does not exist or is not readable"); return; } $TheFile = $filename; $self->SUPER::_resolve_boolexpr(@_); } 1; Compile.pm100664023532023421 233312544604516 21051 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Compile; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_brief { "Attempts to compile each module in the namespace in its own process." } sub help_synopsis { return <lib_path; my @response = `cd $lib_path; perl -I $lib_path -c $module_file 2>&1`; if (grep { $_ eq "$module_file syntax OK\n" } @response) { print "$module_file syntax OK\n" } else { chomp @response; print "$module_file syntax FAILED\n" . join("\n\t",@response), "\n"; } return 1; } 1; Eval.pm100664023532023421 216112544604516 20347 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Eval; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Evaluate a string of Perl source"; } sub help_synopsis { return <<'EOS'; ur test eval 'print "hello\n"' ur test eval 'print "hello\n"' 'print "goodbye\n"' ur test eval 'print "Testing in the " . \$self->namespace_name . " namespace.\n"' EOS } sub help_detail { return <bare_args) { eval "use Data::Dumper; use YAML; no strict; no warnings; \n" . $src; if ($@) { print STDERR "EXCEPTION:\n$@"; } } return 1; } 1; Run.pm100664023532023421 7713212544604516 20256 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::Run; # # single dash command line params go to perl # double dash command line params go to the script # use warnings; use strict; use File::Temp; # qw/tempdir/; use Path::Class; # qw(file dir); use DBI; use Cwd; use UR; our $VERSION = "0.44"; # UR $VERSION; use File::Find; use TAP::Harness; use TAP::Formatter::Console; use TAP::Parser::Aggregator; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1, is_input => 1 }, recurse => { is => 'Boolean', doc => 'Run all .t files in the current directory, and in recursive subdirectories.' }, list => { is => 'Boolean', doc => 'List the tests, but do not actually run them.' }, noisy => { is => 'Boolean', doc => "doesn't redirect stdout",is_optional => 1 }, perl_opts => { is => 'String', doc => 'Override options to the Perl interpreter when running the tests (-d:Profile, etc.)', is_optional => 1, default_value => '' }, lsf => { is => 'Boolean', doc => 'If true, tests will be submitted as jobs via bsub' }, color => { is => 'Boolean', doc => 'Use TAP::Harness::Color to generate color output', default_value => 0 }, junit => { is => 'Boolean', doc => 'Run all tests with junit style XML output. (requires TAP::Formatter::JUnit)' }, ], has_optional => [ 'time' => { is => 'String', doc => 'Write timelog sum to specified file', }, long => { is => 'Boolean', doc => 'Run tests including those flagged as long', }, cover => { is => 'List', doc => 'Cover only this(these) modules', }, cover_svn_changes => { is => 'Boolean', doc => 'Cover modules modified in svn status', }, cover_svk_changes => { is => 'Boolean', doc => 'Cover modules modified in svk status', }, cover_cvs_changes => { is => 'Boolean', doc => 'Cover modules modified in cvs status', }, cover_git_changes => { is => 'Boolean', doc => 'Cover modules modified in git status', }, coverage => { is => 'Boolean', doc => 'Invoke Devel::Cover', }, script_opts => { is => 'String', doc => 'Override options to the test case when running the tests (--dump-sql --no-commit)', default_value => '' }, callcount => { is => 'Boolean', doc => 'Count the number of calls to each subroutine/method', }, jobs => { is => 'Number', doc => 'How many tests to run in parallel', default_value => 1, }, lsf_params => { is => 'String', doc => 'Params passed to bsub while submitting jobs to lsf', default_value => '-q short -R select[type==LINUX64]' }, run_as_lsf_helper => { is => 'String', doc => 'Used internally by the test harness', }, inc => { is => 'String', doc => 'Additional paths for @INC, alias for -I', is_many => 1, }, ], ); sub help_brief { "Run the test suite against the source tree." } sub help_synopsis { return <<'EOS' cd MyNamespace ur test run --recurse # run all tests in the namespace or under the current directory ur test run # runs all tests in the t/ directory under pwd ur test run t/mytest1.t My/Class.t # run specific tests ur test run -v -t --cover-svk-changes # run tests to cover latest svk updates ur test run -I ../some/path/ # Adds ../some/path to perl's @INC through -I ur test run --junit # writes test output in junit's xml format (consumable by Hudson integration system) EOS } sub help_detail { return <define_boolexpr(@_); unless ($bx->specifies_value_for('namespace_name')) { my $namespace_name = $class->resolve_namespace_name_from_cwd(); $namespace_name ||= 'UR'; # Pretend we're running in the UR namespace $bx = $bx->add_filter(namespace_name => $namespace_name); } return $class->SUPER::create($bx); } # Override so we'll allow '-I' on the command line sub _shell_args_getopt_specification { my $self = shift; my($params_hash, @spec) = $self->SUPER::_shell_args_getopt_specification(); foreach (@spec) { if ($_ eq 'inc=s@') { $_ = 'inc|I=s@'; last; } } return($params_hash, @spec); } sub execute { my $self = shift; #$DB::single = 1; my $working_path; if ($self->namespace_name ne 'UR') { $self->status_message("Running tests within namespace ".$self->namespace_name); $working_path = $self->namespace_path; } else { $self->status_message("Running tests under the current directory"); $working_path = '.'; } if ($self->run_as_lsf_helper) { $self->_lsf_test_worker($self->run_as_lsf_helper); exit(0); } # nasty parsing of command line args # this may no longer be needed.. my @tests = $self->bare_args; if ($self->recurse) { if (@tests) { $self->error_message("Cannot currently combine the recurse option with a specific test list."); return; } @tests = $self->_find_t_files_under_directory($working_path); } elsif (not @tests) { my @dirs; File::Find::find(sub { if ($_ eq 't' and -d $_) { push @dirs, $File::Find::name; } }, $working_path); if (@dirs == 0) { $self->error_message("No 't' directories found. Write some tests."); return; } chomp @dirs; for my $dir (@dirs) { push @tests, $self->_find_t_files_under_directory($dir); } } else { # rely on the @tests list from the cmdline } # uniqify and sort them my %tests = map { $_ => 1 } @tests; @tests = sort keys %tests; if ($self->list) { $self->status_message("Tests:"); for my $test (@tests) { $self->status_message($test); } return 1; } if (not @tests) { $self->error_message("No tests found under $working_path"); return; } my $results = $self->_run_tests(@tests); return $results; } sub _find_t_files_under_directory { my($self,$path) = @_; my @tests; File::Find::find(sub { if (m/\.t$/ and not -d $_) { push @tests, $File::Find::name; } }, $path); chomp @tests; return @tests; } # Run by the test harness when test are scheduled out via LSF # $master_spec is a string like "host:port" sub _lsf_test_worker { my($self,$master_spec) = @_; require IO::Socket; open my $saved_stdout, ">&STDOUT" or die "Can't dup STDOUT: $!"; open my $saved_stderr, ">&STDERR" or die "Can't dup STDERR: $!"; while(1) { open STDOUT, ">&", $saved_stdout or die "Can't restore stdout \$saved_stdout: $!"; open STDERR, ">&", $saved_stderr or die "Can't restore stderr \$saved_stderr: $!"; my $socket = IO::Socket::INET->new( PeerAddr => $master_spec, Proto => 'tcp'); unless ($socket) { die "Can't connect to test master: $!"; } $socket->autoflush(1); my $line = <$socket>; chomp($line); if ($line eq '' or $line eq 'EXIT TESTS') { # print STDERR "Closing\n"; $socket->close(); exit(0); } # print "Running >>$line<<\n"; open STDOUT, ">&", $socket or die "Can't redirect stdout: $!"; open STDERR, ">&", $socket or die "Can't redirect stderr: $!"; system($line); $socket->close(); } } sub _run_tests { my $self = shift; my @tests = @_; # this ensures that we don't see warnings # and error statuses when doing the bulk test no warnings; local $ENV{UR_TEST_QUIET} = $ENV{UR_TEST_QUIET}; unless (defined $ENV{UR_TEST_QUIET}) { $ENV{UR_TEST_QUIET} = 1; } use warnings; local $ENV{UR_DBI_NO_COMMIT} = 1; if($self->long) { # Make sure long tests run $ENV{UR_RUN_LONG_TESTS}=1; } my @cover_specific_modules; if (my $cover = $self->cover) { push @cover_specific_modules, @$cover; } if ($self->cover_svn_changes) { push @cover_specific_modules, get_status_file_list('svn'); } elsif ($self->cover_svk_changes) { push @cover_specific_modules, get_status_file_list('svk'); } elsif ($self->cover_git_changes) { push @cover_specific_modules, get_status_file_list('git'); } elsif ($self->cover_cvs_changes) { push @cover_specific_modules, get_status_file_list('cvs'); } if (@cover_specific_modules) { my $dbh = DBI->connect("dbi:SQLite:/gsc/var/cache/testsuite/coverage_metrics.sqlitedb","",""); $dbh->{PrintError} = 0; $dbh->{RaiseError} = 1; my %tests_covering_specified_modules; for my $module_name (@cover_specific_modules) { my $module_test_names = $dbh->selectcol_arrayref( "select test_name from test_module_use where module_name = ?",undef,$module_name ); for my $test_name (@$module_test_names) { $tests_covering_specified_modules{$test_name} ||= []; push @{ $tests_covering_specified_modules{$test_name} }, $module_name; } } if (@tests) { # specific tests were listed: only run the intersection of that set and the covering set my @filtered_tests; for my $test_name (sort keys %tests_covering_specified_modules) { my $specified_modules_coverted = $tests_covering_specified_modules{$test_name}; $test_name =~ s/^(.*?)(\/t\/.*)$/$2/g; if (my @matches = grep { $test_name =~ $_ } @tests) { if (@matches > 1) { Carp::confess("test $test_name matches multiple items in the tests on the filesystem: @matches"); } elsif (@matches == 0) { Carp::confess("test $test_name matches nothing in the tests on the filesystem!"); } else { print STDERR "Running $matches[0] for modules @$specified_modules_coverted.\n"; push @filtered_tests, $matches[0]; } } } @tests = @filtered_tests; } else { # no tests explicitly specified on the command line: run exactly those which cover the listed modules @tests = sort keys %tests_covering_specified_modules; } print "Running the " . scalar(@tests) . " tests which load the specified modules.\n"; } else { } use Cwd; my $cwd = cwd(); for (@tests) { s/^$cwd\///; } my $perl_opts = $self->perl_opts; if ($self->coverage()) { $perl_opts .= ' -MDevel::Cover'; } if ($self->callcount()) { $perl_opts .= ' -d:callcount'; } if (UR::Util::used_libs()) { $ENV{'PERL5LIB'} = UR::Util::used_libs_perl5lib_prefix() . $ENV{'PERL5LIB'}; } my %harness_args; my $formatter; if ($self->junit) { eval "use TAP::Formatter::JUnit;"; if ($@) { Carp::croak("Couldn't use TAP::Formatter::JUnit for junit output: $@"); } %harness_args = ( formatter_class => 'TAP::Formatter::JUnit', merge => 1, timer => 1, ); } else { $formatter = TAP::Formatter::Console->new( { jobs => $self->jobs, show_count => 1, color => $self->color, } ); $formatter->quiet(); %harness_args = ( formatter => $formatter ); } $harness_args{'jobs'} = $self->jobs if ($self->jobs > 1); if ($self->script_opts) { my @opts = split(/\s+/, $self->script_opts); $harness_args{'test_args'} = \@opts; } $harness_args{'multiplexer_class'} = 'My::TAP::Parser::Multiplexer'; $harness_args{'scheduler_class'} = 'My::TAP::Parser::Scheduler'; if ($self->perl_opts || $self->inc) { $harness_args{'switches'} = [ split(' ', $self->perl_opts), map { '-I' . Path::Class::Dir->new($_)->absolute } $self->inc]; } my $timelog_sum = $self->time(); my $timelog_dir; if ($timelog_sum) { $harness_args{'parser_class'} = 'My::TAP::Parser::Timer'; $timelog_sum = Path::Class::file($timelog_sum); $timelog_dir = Path::Class::dir(File::Temp::tempdir('.timelog.XXXXXX', DIR => '.', CLEANUP => 1)); My::TAP::Parser::Timer->set_timer_info($timelog_dir,\@tests); } my $harness = TAP::Harness->new( \%harness_args); if ($self->lsf) { # There doesn't seem to be a clean way (either by configuring the harness, # subclassing the harness or parser, or hooking to a callback) to pass # down the user's requested lsf params from here. So, looks like we # need to hack it through here. This means that multiple 'ur test' commands # running concurrently and using lsf will always use the last object's lsf_params. # though I doubt anyone would ever really need to do that... My::TAP::Parser::IteratorFactory::LSF->lsf_params($self->lsf_params); My::TAP::Parser::IteratorFactory::LSF->max_jobs($self->jobs); $harness->callback('parser_args', sub { my($args, $job_as_arrayref) = @_; $args->{'iterator_factory_class'} = 'My::TAP::Parser::IteratorFactory::LSF'; }); } my $aggregator = TAP::Parser::Aggregator->new(); $aggregator->start(); my $old_stderr; unless ($self->noisy) { open $old_stderr ,">&STDERR" or die "Failed to save STDERR"; open(STDERR,">/dev/null") or die "Failed to redirect STDERR"; } eval { no warnings; local %SIG = %SIG; delete $SIG{__DIE__}; $ENV{UR_DBI_NO_COMMIT} = 1; #$DB::single = 1; $SIG{'INT'} = sub { print "\n\nInterrupt.\nWaiting for running tests to finish...\n\n"; $My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT = 1; $SIG{'INT'} = 'DEFAULT'; #My::TAP::Parser::IteratorFactory::LSF->_kill_running_jobs(); #sleep(1); #$aggregator->stop(); #$formatter->summary($aggregator); #exit(0); }; #runtests(@tests); $harness->aggregate_tests( $aggregator, @tests ); }; unless ($self->noisy) { open(STDERR,">&", $old_stderr) or die "Failed to restore STDERR"; } $aggregator->stop(); if ($@) { $self->error_message($@); return; } else { if ($self->coverage()) { # FIXME - is this GSC-specific? system("chmod -R g+rwx cover_db"); system("/gsc/bin/cover | tee > coverage.txt"); } $formatter->summary($aggregator) if ($formatter); } if ($timelog_sum) { $timelog_sum->openw->print( sort map { $_->openr->getlines } $timelog_dir->children ); if (-z $timelog_sum) { unlink $timelog_sum; warn "Error producing time summary file!"; } $timelog_dir->rmtree; } return !$aggregator->has_problems; } sub get_status_file_list { my $tool = shift; my @status_data = eval { my $orig_cwd = cwd(); my @words = grep { length($_) } split("/",$orig_cwd); while (@words and ($words[-1] ne "GSC")) { pop @words; } unless (@words and $words[-1] eq "GSC") { die "Cannot find 'GSC' directory above the cwd. Cannot auto-run $tool status.\n"; } pop @words; my $vcs_dir = "/" . join("/", @words); unless (chdir($vcs_dir)) { die "Failed to change directories to $vcs_dir!"; } my @lines; if ($tool eq "svn" or $tool eq "svk") { @lines = IO::File->new("$tool status |")->getlines; } elsif ($tool eq "cvs") { @lines = IO::File->new("cvs -q up |")->getlines; } elsif ($tool eq "git") { @lines = IO::File->new("git diff --name-status |")->getlines; } else { die "Unknown tool $tool. Try svn, svk, cvs or git.\n"; } # All these tools have flags or other data with the filename as the last column @lines = map { (split(/\s+/))[-1] } @lines; unless (chdir($orig_cwd)) { die "Error changing directory back to the original cwd after checking file status with $tool."; } return @lines; }; if ($@) { die "Error checking version control status for $tool:\n$@"; } my @modules; for my $line (@status_data) { my ($status,$file) = ($line =~ /^(.).\s*(\S+)/); next if $status eq "?" or $status eq "!"; print "covering $file\n"; push @modules, $file; } unless (@modules) { die "Failed to find modified modules via $tool.\n"; } return @modules; } package My::TAP::Parser::Multiplexer; use base 'TAP::Parser::Multiplexer'; sub _iter { my $self = shift; my $original_iter = $self->SUPER::_iter(@_); return sub { for(1) { # This is a hack... # the closure _iter returns does a select() on the subprocess' output handle # which returns immediately after you hit control-C with no results, and the # existing code in there expects real results from select(). This way, we catch # the exception that happens when you do that, and give it a chance to try again my @retval = eval { &$original_iter }; if (index($@, q(Can't use an undefined value as an ARRAY reference))>= 0) { redo; } elsif ($@) { die $@; } return @retval; } }; } package My::TAP::Parser::IteratorFactory::LSF; use IO::Socket; use IO::Select; use base 'TAP::Parser::IteratorFactory'; # Besides being the factory for parser iterators, we're also the factory for # LSF jobs # In the TAP::* code, they mention that the iterator factory is never instantiated, # but may be in the future. When that happens, move this state info into the # object that gets created/initialized my $state = { 'listen' => undef, # The listening socket 'select' => undef, # select object for the listen socket idle_jobs => [], # holds a list of file handles of connected workers # running_jobs => [], # we're not tracking workers that are working for now... lsf_jobids => [], # jobIDs of the worker processes lsf_params => '', # params when running bsub max_jobs => 0, # Max number of jobs }; sub _kill_running_jobs { # The worker processes should notice when the master goes away, # but just in case, we'll kill them off foreach my $jobid ( @{$state->{'lsf_jobids'}} ) { print "bkilling LSF jobid $jobid\n"; `bkill $jobid`; } } END { my $exit_code = $?; &_kill_running_jobs(); $? = $exit_code; # restore the exit code, since the bkill commands set a different exit code } sub lsf_params { my $proto = shift; if (@_) { $state->{'lsf_params'} = shift; } return $state->{'lsf_params'}; } sub max_jobs { my $proto = shift; if (@_) { $state->{'max_jobs'} = shift; } return $state->{'max_jobs'}; } sub make_process_iterator { my $proto = shift; My::TAP::Parser::Iterator::Process::LSF->new(@_); } sub next_idle_worker { my $proto = shift; $proto->process_events(); while(! @{$state->{'idle_jobs'}} ) { my $did_create_new_worker = 0; if (@{$state->{'lsf_jobids'}} < $state->{'max_jobs'}) { $proto->create_new_worker(); $did_create_new_worker = 1; } sleep(1); my $count = $proto->process_events($did_create_new_worker ? 10 : 0); if (! $did_create_new_worker and ! $count) { unless ($proto->_verify_lsf_jobs_are_still_alive()) { print "\n*** The LSF worker jobs are having trouble starting up... Exiting\n"; kill 'INT', $$; sleep 2; kill 'INT', $$; } } } my $worker = shift @{$state->{'idle_jobs'}}; return $worker; } sub _verify_lsf_jobs_are_still_alive { my $alive = 0; foreach my $jobid ( @{$state->{'lsf_jobids'}} ) { my @output = `bjobs $jobid`; next unless $output[1]; # expired jobs only have 1 line of output: Job is not found my @stat = split(/\s+/, $output[1]); $alive++ if ($stat[2] eq 'RUN' or $stat[2] eq 'PEND'); } return $alive; } #sub worker_is_now_idle { # my($proto, $worker) = @_; # # for (my $i = 0; $i < @{$state->{'running_jobs'}}; $i++) { # if ($state->{'running_jobs'}->[$i] eq $worker) { # splice(@{$state->{'running_jobs'}}, $i, 1); # last; # } # } # # push @{$state->{'idle_workers'}}, $worker; #} sub create_new_worker { my $proto = shift; my $port = $state->{'listen'}->sockport; my $host = $state->{'listen'}->sockhost; if ($host eq '0.0.0.0') { $host = $ENV{'HOST'}; } $host .= ":$port"; my $lsf_params = $state->{'lsf_params'} || ''; my $line = `bsub $lsf_params ur test run --run-as-lsf-helper $host`; my ($jobid) = $line =~ m/Job \<(\d+)\>/; unless ($jobid) { Carp::croak("Couldn't parse jobid out of the line: $line"); } push @{$state->{'lsf_jobids'}}, $jobid; } sub process_events { my $proto = shift; my $timeout = shift || 0; my $listen = $state->{'listen'}; unless ($listen) { $listen = $state->{'listen'} = IO::Socket::INET->new(Listen => 5, Proto => 'tcp'); unless ($listen) { Carp::croak("Unable to create listen socket: $!"); } } my $select = $state->{'select'}; unless ($select) { $select = $state->{'select'} = IO::Select->new($listen); } my $processed_events = 0; while(1) { my @ready = $select->can_read($timeout); last unless (@ready); foreach my $handle ( @ready ) { $processed_events++; if ($handle eq $listen) { my $socket = $listen->accept(); unless ($socket) { Carp::croak("accept: $!"); } $socket->autoflush(1); push @{$state->{'idle_jobs'}}, $socket; } else { # shoulnd't get here... } $timeout = 0; # just do a poll() next time around } } return $processed_events; } package My::TAP::Parser::Timer; use base 'TAP::Parser'; our $timelog_dir; our $test_list; sub set_timer_info { my($class,$time_dir,$testlist) = @_; $timelog_dir = $time_dir; $test_list = $testlist; } sub make_iterator { my $self = shift; my $args = $_[0]; if (ref($args) eq 'HASH') { # It's about to make a process iterator. Prepend the stuff to # run the timer, too unless (-d $timelog_dir) { File::Path::mkpath("$timelog_dir"); } my $timelog_file = $self->_timelog_file_for_command_list($args->{'command'}); my $format = q('%C %e %U %S %I %K %P'); # yes, that's single quotes inside q() unshift @{$args->{'command'}}, '/usr/bin/time', '-o', $timelog_file, '-a', '-f', $format; } $self->SUPER::make_iterator(@_); } sub _timelog_file_for_command_list { my($self,$command_list) = @_; foreach my $test_file ( @$test_list ) { foreach my $cmd_part ( reverse @$command_list ) { if ($test_file eq $cmd_part) { my $log_file = Path::Class::file($cmd_part)->basename; $log_file =~ s/\.t$//; $log_file .= sprintf('.%d.%d.time', time(), $$); # Try to make the name unique $log_file = $timelog_dir->file($log_file); $log_file->openw->close(); return $log_file; } } } Carp::croak("Can't determine time log file for command line: ",join(' ',@$command_list)); } package My::TAP::Parser::Scheduler; use base 'TAP::Parser::Scheduler'; sub get_job { my $self = shift; if ($My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT) { our $already_printed; unless ($already_printed) { print "\n\n ",$self->{'count'}," Tests not yet run before interrupt\n"; print "------------------------------------------\n"; foreach my $job ( $self->get_all ) { print $job->{'description'},"\n"; } print "------------------------------------------\n"; $already_printed = 1; } return; } $self->SUPER::get_job(@_); } package My::TAP::Parser::Iterator::Process::LSF; our $SHOULD_EXIT = 0; use base 'TAP::Parser::Iterator::Process'; sub _initialize { my($self, $args) = @_; my @command = @{ delete $args->{command} || [] } or die "Must supply a command to execute"; # From TAP::Parser::Iterator::Process my $chunk_size = delete $args->{_chunk_size} || 65536; if ( my $setup = delete $args->{setup} ) { $setup->(@command); } my $handle = My::TAP::Parser::IteratorFactory::LSF->next_idle_worker(); # Tell the worker to run the command unless($handle->print(join(' ', @command) . "\n")) { print "Couldn't send command to worker on host ".$handle->peeraddr." port ".$handle->peerport.": $!\n"; print "Handle is " . ( $handle->connected ? '' : '_not_' ) . " connected\n"; } $self->{'out'} = $handle; $self->{'err'} = ''; $self->{'sel'} = undef; #IO::Select->new($handle); $self->{'pid'} = undef; $self->{'chunk_size'} = $chunk_size; if ( my $teardown = delete $args->{teardown} ) { $self->{teardown} = sub { $teardown->(@command); }; } return $self; } sub next_raw { my $self = shift; My::TAP::Parser::IteratorFactory::LSF->process_events(); if ($SHOULD_EXIT) { #$DB::single = 1; if ($self->{'sel'}) { foreach my $h ( $self->{'sel'}->handles ) { $h->close; $self->{'sel'}->remove($h); } return "1..0 # Skipped: Interrupted by user"; } else { return; } } $self->SUPER::next_raw(@_); } #sub _finish { # my $self = shift; # # $self->SUPER::_finish(@_); # # My::TAP::Parser::IteratorFactory::LSF->worker_is_now_idle($handle); #} 1; =pod =head1 NAME ur test run - run one or more test scripts =head1 SYNOPSIS # run everything in a given namespace cd my_sandbox/TheNamespace ur test run --recurse # run only selected tests cd my_sandbox/TheNamespace ur test run My/Module.t Another/Module.t t/foo.t t/bar.t # run only tests which load the TheNamespace::DNA module cd my_sandbox/TheNamespace ur test run --cover TheNamespace/DNA.pm # run only tests which cover the changes you have in Subversion cd my_sandbox/TheNamespace ur test run --cover-svn-changes # run 5 tests in parallel as jobs scheduled via LSF cd my_sandbox/TheNamespace ur test run --lsf --jobs 5 =head1 DESCRIPTION Runs a test harness around automated test cases, like "make test" in a make-oriented software distrbution, and similar to "prove" run in bulk. When run w/o parameters, it looks for "t" directory in the current working directory, and runs ALL tests under that directory. =head1 OPTIONS =over 4 =item --recurse Run all tests in the current directory, and in sub-directories. Without --recurse, it will first recursively search for directories named 't' under the current directory, and then recursively seatch for *.t files under those directories. =item --long Include "long" tests, which are otherwise skipped in test harness execution =item -v Be verbose, meaning that individual cases will appear instead of just a full-script summary =item --cover My/Module.pm Looks in a special sqlite database which is updated by the cron which runs tests, to find all tests which load My/Module.pm at some point before they exit. Only these tests will be run. * you will still need the --long flag to run long tests. * if you specify tests on the command-line, only tests in both lists will run * this can be specified multiple times =item --cover-TOOL-changes TOOL can be svn, svk, or cvs. The script will run either "svn status", "svk status", or "cvs -q up" on a parent directory with "GSC" in it, and get all of the changes in your perl_modules trunk. It will behave as though those modules were listed as individual --cover options. =item --lsf Tests should not be run locally, instead they are submitted as jobs to the LSF cluster with bsub. =item --lsf-params Parameters given to bsub when sceduling jobs. The default is "-q short -R select[type==LINUX64]" =item --jobs This many tests should be run in parallel. If --lsf is also specified, then these parallel tests will be submitted as LSF jobs. =back =head1 PENDING FEATURES =over 4 =item automatic remote execution for tests requiring a distinct hardware platform =item logging profiling and coverage metrics with each test =back =cut TrackObjectRelease.pm100664023532023421 641612544604516 23163 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::TrackObjectRelease; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; class UR::Namespace::Command::Test::TrackObjectRelease { is => 'UR::Namespace::Command::Base', has => [ file => { is => 'Text', doc => 'pathname of the input file' }, ], }; sub help_brief { 'Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks' }; sub help_synopsis { "ur test track-object-release --file /path/to/text.file > /path/to/results" } sub help_detail { "When a UR-based program is run with the UR_DEBUG_OBJECT_RELEASE environment variable set to 1, it will emit messages to STDERR describing the various stages of releasing an object. This command parses those messages and provides a report on objects which did not completely deallocate themselves, usually because of a reference being held." } sub execute { my $self = shift; #$DB::single = 1; my $file = $self->file; my $fh = IO::File->new($file,'r'); unless ($fh) { $self->error_message("Can't open input file: $!"); return; } # for a given state, it's legal predecessor my %prev_states = ( 'PRUNE object' => '', 'DESTROY object' => 'PRUNE object', 'UNLOAD object' => 'DESTROY object', 'DELETE object' => 'UNLOAD object', 'BURY object' => 'DELETE object', 'DESTROY deletedref' => 'BURY object', ); my %next_states = reverse %prev_states; # After this we stop stracking it my %terminal_states = ( 'DESTROY deletedref' => 1 ); my %objects; while(<$fh>) { chomp; my ($action,$refaddr); if (m/MEM ((PRUNE|DESTROY|UNLOAD|DELETE|BURY) (object|deletedref)) (\S+)/) { $action = $1; my $refstr = $4; ($refaddr) = ($refstr =~ m/=HASH\((.*)\)/); } else { next; } my($class,$id) = m/class (\S+) id (.*)/; # These don't appear in the deletedref line, and are optional my $expected_prev_state = $prev_states{$action}; if (defined $expected_prev_state && $expected_prev_state) { # This state must have a predecessor if ($objects{$expected_prev_state}->{$refaddr}) { if ($terminal_states{$action}) { delete $objects{$expected_prev_state}->{$refaddr}; } else { $objects{$action}->{$refaddr} = delete $objects{$expected_prev_state}->{$refaddr}; } } else { print STDERR "$action for $refaddr without matching $expected_prev_state at line $.\n"; } } elsif (defined $expected_prev_state) { # The initial state $objects{$action}->{$refaddr} = $_; } else { print STDERR "Unknown action $action at line $.\n"; } } foreach my $action (keys %objects) { if (keys %{$objects{$action}} ) { print "\n$action but not $next_states{$action}\n"; foreach (keys %{$objects{$action}}) { print "$_ : ",$objects{$action}->{$_},"\n"; } } } return 1; } 1; Use.pm100664023532023421 715612544604516 20225 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Use; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use Cwd; use YAML; class UR::Namespace::Command::Test::Use { is => "UR::Namespace::Command::RunsOnModulesInTree", has_optional => [ verbose => { is => 'Boolean', doc => 'List each explicitly.' }, summarize_externals => { is => 'Boolean', doc => 'List all modules used which are outside the namespace.' }, exec => { is => 'Text', doc => 'Execute the specified Perl _after_ using all of the modules.' }, ] }; sub help_brief { "Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to \@INC by any modules (bad!)." } sub help_synopsis { return <_help_detail_footer; return $text; } sub before { my $self = shift; $self->{success} = 0; $self->{failure} = 0; $self->{used_libs} = {}; $self->{used_mods} = {}; $self->{failed_libs} = []; $self->{default_print_fh} = fileno(select); $self->SUPER::before(@_); } sub for_each_module_file { my $self = shift; my $module_file = shift; my $namespace_name = $self->namespace_name; my %libs_before = map { $_ => 1 } @INC; my %mods_before = %INC if $self->summarize_externals; local $SIG{__DIE__}; local $ENV{UR_DBI_MONITOR_SQL} = 1; local $ENV{APP_DBI_MONITOR_SQL} = 1; local *CORE::GLOBAL::exit = sub {}; $self->debug_message("require $module_file"); eval "require '$module_file'"; my %new_libs = map { $_ => 1 } grep { not $libs_before{$_} } @INC; my %new_mods = map { $_ => $module_file } grep { not $_ =~ /^$namespace_name\// } grep { not $mods_before{$_} } keys %INC; if (%new_libs) { $self->{used_libs}{$module_file} = \%new_libs; } if (%new_mods) { for my $mod (keys %new_mods) { $self->{used_mods}{$mod} = $module_file; } } if ($@) { print "$module_file FAILED:\n$@\n"; $self->{failure}++; push @{$self->{failed_libs}}, $module_file; } elsif (fileno(select) != $self->{default_print_fh}) { # un-steal the default file handle back select(STDOUT); print "$module_file FAILED DUE TO IMPROPER FILEHANDLE USE\n"; $self->{failure}++; push @{$self->{failed_libs}}, $module_file; } else { print "$module_file OK\n" if $self->verbose; $self->{success}++; } return 1; } sub after { my $self = shift; $self->status_message("SUCCESS: $self->{success}"); $self->status_message("FAILURE: $self->{failure}"); if ($self->{failure} > 0) { $self->status_message("FAILED LIBS: " . YAML::Dump($self->{failed_libs})); } if (%{ $self->{used_libs} }) { $self->status_message( "ROGUE LIBS: " . YAML::Dump($self->{used_libs}) ) } if ($self->summarize_externals) { $self->status_message( "MODULES USED: " . YAML::Dump($self->{used_mods}) ); } if (my $src = $self->exec) { eval $src; $self->error_message($@) if $@; } return if $self->{failure}; return 1; } 1; Window.pm100664023532023421 1361512544604516 20755 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Test use strict; use warnings; use above 'UR'; package UR::Namespace::Command::Test::Window; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', has => { src => { is_optional => 1, is_many => 1, shell_args_position => 1 } }, doc => 'repl tk window' ); sub execute { my $self = shift; require Tk; my $src = [$self->src]; if (@$src > 0) { for my $code (@$src) { no strict; no warnings; eval $code; print $@; } } else { UR::Namespace::Command::Test::Window::Tk->activate_with_gtk_support; } } package UR::Namespace::Command::Test::Window::Tk;; our $tmp; our $workspace; sub new { if ($^O eq 'MSWin32' || $^O eq 'cygwin') { #$tmp = $ENV{TEMP}; $tmp ||= 'C:/temp'; } else { $tmp = $ENV{TMPDIR}; $tmp ||= '/tmp'; } # Make a window my $debug_window = new MainWindow(-title=>"Debug"); # The top pane is for inputing Perl my $in = $debug_window->Scrolled("Text", -scrollbars=>'os',-exportselection => 1)->pack(-expand => 0, -fill => 'x'); # The middle is a frame with an eval button my $frame = $debug_window->Frame()->pack(-expand=>0,-fill=>'both',-anchor=>'nw'); my $go = $frame->Button(-text => 'eval()', -command => sub { &exec_debug($debug_window) } )->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); # The bottom is a pane for output my $out = $debug_window->Scrolled("Text", -scrollbars=>'osoe',-wrap => 'none')->pack(-fill => 'both', -expand => 1); # See if there is a workspace file for an app with this name my $user = $ENV{USER}; $user ||= 'anonymous'; $0 =~ /([^\/\s]+)$/; my $core_name = $1; $workspace ||= "$user\@$core_name"; print STDOUT "Workspace is $workspace\n"; if (open(LAST_WORKSPACE,"${tmp}/$workspace")) { while () { $in->insert("end",$_); } close LAST_WORKSPACE; } $debug_window->{in} = $in; $debug_window->{out} = $out; return $debug_window; } sub new_gtk { require Gtk; my $debug_window = DebugWindow::new(); my $frame = $debug_window->Frame()->pack(-expand=>0,-fill=>'both',-anchor=>'nw'); my $continuous_refresh = 0; $frame->Button(-text => 'One Gtk', -command => sub { Gtk->main_iteration })->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); $frame->Button(-text => 'All Gtk', -command => sub { while (Gtk->events_pending) { Gtk->main_iteration; } })->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); my $handleGtk; $handleGtk = sub { return unless (Exists($debug_window) and $continuous_refresh); Gtk->main_iteration; my $delay = (Gtk->events_pending ? 5 : 500); Tk->after($delay, $handleGtk); }; $frame->Button ( -text => 'Gtk Cont', -command => sub { $continuous_refresh = (not $continuous_refresh); &$handleGtk; } )->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); } sub activate { my $window=&new; $window->waitWindow(); Tk->MainLoop; } sub activate_with_gtk_support { &new_gtk; Tk->MainLoop; } sub hook_button { } sub hook_gtk_button { my $gtk_button = shift; $gtk_button->signal_connect('button_press_event', sub { my ($self,$event) = @_; # Test the Gtk widget event to see which button was clicked. if ($event->{button} == 3) { # Instantiate the debug window with the special Gtk buttons on it. my $debug_window = DebugWindow::new_gtk(); Tk->MainLoop(); } return(1); }); } sub show_new { # Legacy function &new(@_); } sub exec_debug { my $self = $_[0]; my $in = $self->{in}; my $out = $self->{out}; # Clear the results window. $out->delete("1.0","end"); # Get all of the text in the workspace window. my $perl = $in->get("1.0","end"); # If there is a valid selection override the above with just the selected text. eval { $perl = $in->get("sel.first","sel.last"); }; # Open a temporary output file to catch the STDOUT my $filename = "${tmp}/${workspace}_output"; open (DEBUG_FH,">$filename") or die "Failed to open temp file '$filename': $!\n"; # Redirect STDOUT temporarily *ORIG_STDOUT = *STDOUT; *STDOUT = *DEBUG_FH; # Run the perl. eval ("package main;\n" . $perl); # Print any errors print $@; # Restore STDOUT *STDOUT = *ORIG_STDOUT; close DEBUG_FH; # Get the script output my $fh = IO::File->new("${tmp}/${workspace}_output"); my $text; if ($fh) { my @text = $fh->getlines; $fh->close; $text = join("",@text); } # Print to the console as well as the result widget. print $text; # For some reason embedded \n causese every other row to disappear. # Split on line boundaries and feed the output to the widget in pieces. foreach my $row (split /$/, $text) { $out->insert('end',$row); } # Save the whole workspace like we do when the app closes save_workspace($self); } sub save_workspace { my $self = $_[0]; my $in = $self->{in}; # Save the workspace if (open (SCRIPT_FH, ">${tmp}/$workspace")) { print SCRIPT_FH $in->get("1.0","end"); close SCRIPT_FH; print "Saved to ${tmp}/$workspace\n"; } else { print STDOUT "Failed to save the current workspace (${tmp}/$workspace)!"; } } 1; Update.pm100664023532023421 50012544604516 17736 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Update; use warnings; use strict; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => 'update parts of the source tree of a UR namespace' ); sub sub_command_sort_position { 4 } 1; ClassDiagram.pm100664023532023421 3236212544604516 22343 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::ClassDiagram; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ data_source => {type => 'String', doc => 'Which datasource to use', is_optional => 1}, depth => { type => 'Integer', doc => 'Max distance of related classes to include. Default is 1. 0 means show only the named class(es), -1 means to include everything', is_optional => 1}, file => { type => 'String', doc => 'Pathname of the Umlet (.uxf) file' }, show_attributes => { type => 'Boolean', is_optional => 1, default => 1, doc => 'Include class attributes in the diagram' }, show_methods => { type => 'Boolean', is_optional => 1, default => 0, doc => 'Include methods in the diagram (not implemented yet' }, include_ur_object => { type => 'Boolean', is_optional => 1, default => 0, doc => 'Include UR::Object and UR::Entity in the diagram (default = no)' }, initial_name => { is_many => 1, is_optional => 1, shell_args_position => 1 } ], ); sub sub_command_sort_position { 4 }; sub help_brief { "Update an Umlet diagram based on the current class definitions" } sub help_detail { return < 800; sub execute { my $self = shift; my $params = shift; #$DB::single = 1; my $namespace = $self->namespace_name; eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } my @initial_name_list = $self->initial_name; my $diagram; if (-f $params->{'file'}) { $params->{'depth'} = 0 unless (exists $params->{'depth'}); # Default is just update what's there $diagram = UR::Object::Umlet::Diagram->create_from_file($params->{'file'}); push @initial_name_list, map { $_->subject_id } UR::Object::Umlet::Class->get(diagram_name => $diagram->name); } else { $params->{'depth'} = 1 unless exists($params->{'depth'}); $diagram = UR::Object::Umlet::Diagram->create(name => $params->{'file'}); } # FIXME this can get removed when attribute defaults work correctly unless (exists $params->{'show_attributes'}) { $self->show_attributes(1); } my @involved_classes; foreach my $class_name ( @initial_name_list ) { push @involved_classes, UR::Object::Type->get(class_name => $class_name); } push @involved_classes, $self->_get_related_classes_via_inheritance( names => \@initial_name_list, depth => $params->{'depth'}, ); push @involved_classes, $self->_get_related_classes_via_properties( #names => [ map { $_->class_name } @involved_classes ], names => \@initial_name_list, depth => $params->{'depth'}, ); my %involved_class_names = map { $_->class_name => $_ } @involved_classes; # The initial placement, and how much to move over for the next box my($x_coord, $y_coord, $x_inc, $y_inc) = (20,20,40,40); my @objs = sort { $b->y <=> $a->y or $b->x <=> $a->x } UR::Object::Umlet::Class->get(); if (@objs) { my $maxobj = $objs[0]; $x_coord = $maxobj->x + $maxobj->width + $x_inc; $y_coord = $maxobj->y + $maxobj->height + $y_inc; } # First, place all the classes my @all_boxes = UR::Object::Umlet::Class->get( diagram_name => $diagram->name ); foreach my $class ( values %involved_class_names ) { my $umlet_class = UR::Object::Umlet::Class->get(diagram_name => $diagram->name, subject_id => $class->class_name); my $created = 0; unless ($umlet_class) { $created = 1; $umlet_class = UR::Object::Umlet::Class->create( diagram_name => $diagram->name, subject_id => $class->class_name, label => $class->class_name, x => $x_coord, y => $y_coord, ); # add the attributes if ($self->show_attributes) { my $attributes = $umlet_class->attributes || []; my %attributes_already_in_diagram = map { $_->{'name'} => 1 } @{ $attributes }; my %id_properties = map { $_ => 1 } $class->id_property_names; my $line_count = scalar @$attributes; foreach my $property_name ( $class->direct_property_names ) { next if $attributes_already_in_diagram{$property_name}; $line_count++; my $property = UR::Object::Property->get(class_name => $class->class_name, property_name => $property_name); push @$attributes, { is_id => $id_properties{$property_name} ? '+' : ' ', name => $property_name, type => $property->data_type, line => $line_count, }; } $umlet_class->attributes($attributes); } if ($self->show_methods) { # Not implemented yet # Use the same module the schemabrowser uses to get that info } # Make sure this box dosen't overlap other boxes while(my $overlapped = $umlet_class->is_overlapping(@all_boxes) ) { if ($umlet_class->x > MAX_X_AUTO_POSITION) { $umlet_class->x(20); $umlet_class->y( $umlet_class->y + $y_inc); } else { $umlet_class->x( $overlapped->x + $overlapped->width + $x_inc ); } } push @all_boxes, $umlet_class; } if ($created) { $x_coord = $umlet_class->x + $umlet_class->width + $x_inc; if ($x_coord > MAX_X_AUTO_POSITION) { $x_coord = 20; $y_coord += $y_inc; } } } # Next, connect the classes together foreach my $class ( values %involved_class_names ) { my @properties = grep { $_->is_delegated and $_->data_type} $class->all_property_metas(); foreach my $property ( @properties ) { next unless (exists $involved_class_names{$property->data_type}); my @property_links = eval { $property->get_property_name_pairs_for_join }; next unless @property_links; my $id_by = join(':', map { $_->[0] } @property_links); my $their_id_by = join (':', map { $_->[1] } @property_links); my $umlet_relation = UR::Object::Umlet::Relation->get( diagram_name => $diagram->name, from_entity_name => $property->class_name, to_entity_name => $property->data_type, from_attribute_name => $id_by, to_attribute_name => $their_id_by, ); unless ($umlet_relation) { $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<-', from_entity_name => $property->class_name, to_entity_name => $property->data_type, from_attribute_name => $id_by, to_attribute_name => $their_id_by, ); unless ($umlet_relation->connect_entity_attributes()) { # This didn't link to anything on the diagram $umlet_relation->delete; } } } foreach my $parent_class_name ( @{ $class->is } ) { next unless ($involved_class_names{$parent_class_name}); my $umlet_relation = UR::Object::Umlet::Relation->get( diagram_name => $diagram->name, from_entity_name => $class->class_name, to_entity_name => $parent_class_name, ); unless ($umlet_relation) { $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<<-', from_entity_name => $class->class_name, to_entity_name => $parent_class_name, ); $umlet_relation->connect_entities(); } } } $diagram->save_to_file($params->{'file'}); 1; } sub _get_related_classes_via_properties { my($self, %params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; # Make sure the named classes are loaded foreach ( @{ $params{'names'} } ) { eval { $_->class }; } # Get everything linked to the named things my @related_names = grep { eval { $_->class } } #grep { $_ } map { $_->data_type } map { UR::Object::Property->get(class_name => $_ ) } @{ $params{'names'}}; push @related_names, grep { eval { $_->class } } #grep { $_ } map { $_->class_name } map { UR::Object::Property->get(data_type => $_ ) } @{ $params{'names'}}; return unless @related_names; my @objs = map { UR::Object::Type->get(class_name => $_) } @related_names; #my @related_names = grep { $_ } map { $_->$related_param } $related_class->get($item_param => $params{'names'}); #push @related_names, grep { $_ } map { $_->$item_param } $related_class->get($related_param => $params{'names'}); #return unless @related_names; # # my @objs = $item_class->get($item_param => \@related_names); unless ($self->include_ur_object) { # Prune out UR::Object and UR::Entity @objs = grep { $_->class_name ne 'UR::Object' and $_->class_name ne 'UR::Entity' } @objs; } # make a recursive call to get the related objects by name return ( @objs, $self->_get_related_classes_via_properties( %params, names => \@related_names, depth => --$params{'depth'}) ); } sub _get_related_classes_via_inheritance { my($self,%params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; my @related_class_names; foreach my $class_name ( @{ $params{'names'} } ) { # get the class loaded eval { $class_name->class }; if ($@) { $self->warning_message("Problem loading class $class_name: $@"); next; } # Get this class' parents #push @related_class_names, $class_name->parent_classes; push @related_class_names, @{ $class_name->__meta__->is }; } my @objs = map { $_->__meta__ } @related_class_names; unless ($self->include_ur_object) { # Prune out UR::Object and UR::Entity @objs = grep { $_->class_name ne 'UR::Object' and $_->class_name ne 'UR::Entity' } @objs; } # make a recursive call to get their parents return ( @objs, $self->_get_related_classes_via_inheritance( %params, names => \@related_class_names, depth => --$params{'depth'}, ) ); } 1; ClassesFromDb.pm100664023532023421 15647012544604516 22527 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::ClassesFromDb; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use Text::Diff; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ data_source => { is => 'List', is_optional => 1, doc => 'Limit updates to these data sources' }, force_check_all_tables => { is => 'Boolean', is_optional => 1, doc => 'By default we only look at tables with a new DDL time for changed database schema information. This explicitly (slowly) checks each table against our cache.' }, force_rewrite_all_classes => { is => 'Boolean', is_optional => 1, doc => 'By default we only rewrite classes where there are database changes. Set this flag to rewrite all classes even where there are no schema changes.' }, table_name => { is => 'List', is_optional => 1, doc => 'Update the specified table.' }, class_name => { is => 'List', is_optional => 1, doc => 'Update only the specified classes.' }, ], ); sub sub_command_sort_position { 2 }; sub help_brief { "Update class definitions (and data dictionary cache) to reflect changes in the database schema." } sub help_detail { return <SUPER::create(%params); return unless $obj; $obj->{'_override_no_commit_for_filesystem_items'} = $override if $override; return $obj; } our @dd_classes = ( 'UR::DataSource::RDBMS::Table', 'UR::DataSource::RDBMS::TableColumn', 'UR::DataSource::RDBMS::FkConstraint', 'UR::DataSource::RDBMS::Table::Ghost', 'UR::DataSource::RDBMS::TableColumn::Ghost', 'UR::DataSource::RDBMS::FkConstraint::Ghost', ); sub execute { my $self = shift; # # Command parameter checking # my $force_check_all_tables = $self->force_check_all_tables; my $force_rewrite_all_classes = $self->force_rewrite_all_classes; my $namespace = $self->namespace_name; $self->status_message("Updating namespace: $namespace\n"); my @namespace_data_sources = $namespace->get_data_sources; my $specified_table_name_arrayref = $self->table_name; my $specified_data_source_arrayref = $self->data_source; my $specified_class_name_arrayref = $self->class_name; my @data_dictionary_objects; if ($specified_class_name_arrayref or $specified_table_name_arrayref) { my $ds_table_list; if ($specified_class_name_arrayref) { $ds_table_list = [ map { [$_->data_source, $_->table_name] } grep { $_->data_source } map { $_->__meta__ } @$specified_class_name_arrayref ]; } else { $ds_table_list = [ map { [$_->data_source, $_->table_name] } UR::DataSource::RDBMS::Table->get(table_name => $specified_table_name_arrayref) ]; for my $item (@$ds_table_list) { UR::Object::Type->get(data_source => $item->[0], table_name => $item->[1]); } } for my $item (@$ds_table_list) { my ($data_source, $table_name) = @$item; $self->_update_database_metadata_objects_for_schema_changes( data_source => $data_source, force_check_all_tables => $force_check_all_tables, table_name => $table_name, ); for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) { push @data_dictionary_objects, $dd_class->get(data_source_obj => $data_source, table_name => $table_name); } } } else { # Do the update by data source, all or whatever is specified. # # Determine which data sources to update from. # By default, we do all datasources owned by the namespace. # my @target_data_sources; if ($specified_data_source_arrayref) { @target_data_sources = (); my %data_source_is_specified = map { $_ => 1 } @$specified_data_source_arrayref; for my $ds (@namespace_data_sources) { if ($data_source_is_specified{$ds->id}) { push @target_data_sources, $ds; delete $data_source_is_specified{$ds->id}; } } #delete @data_source_is_specified{@namespace_data_sources}; if (my @unknown = keys %data_source_is_specified) { $self->error_message( "Unknown data source(s) for namespace $namespace: @unknown!\n" . "Select from:\n" . join("\n",map { $_->id } @namespace_data_sources) . "\n" ); return; } } else { # Don't update the Meta datasource, unless they specificly asked for it @target_data_sources = grep { $_->id !~ /::Meta$/ } @namespace_data_sources; } # Some data sources can't handle the magic required for automatic class updating... @target_data_sources = grep { $_->can('get_table_names') } @target_data_sources; $self->status_message("Found data sources: " . join(", " , map { /${namespace}::DataSource::(.*)$/; $1 || $_ } map { $_->id } @target_data_sources ) ); # # A copy of the database metadata is in the ::Meta sqlite datasource. # Get updates to it first. # ##$DB::single = 1; for my $data_source (@target_data_sources) { # ensure the class has been lazy-loaded until UNIVERSAL::can is smarter... $data_source->class; $self->status_message("Checking " . $data_source->id . " for schema changes ..."); my $success = $self->_update_database_metadata_objects_for_schema_changes( data_source => $data_source, force_check_all_tables => $force_check_all_tables, ); unless ($success) { return; } } # # Summarize the database changes by table. We'll create/update/delete the class which goes with that table. # ##$DB::single = 1; my $cx = UR::Context->current; for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) { push @data_dictionary_objects, grep { $force_rewrite_all_classes ? 1 : $_->__changes__ or exists($_->{'db_saved_uncommitted'}) } $cx->all_objects_loaded($dd_class); my $ghost_class = $dd_class . "::Ghost"; push @data_dictionary_objects, $cx->all_objects_loaded($ghost_class); } } # The @data_dictionary_objects array has all dd meta which should be used to rewrite classes. my %changed_tables; for my $obj ( @data_dictionary_objects ) { my $table; if ($obj->can("get_table")) { $table = $obj->get_table; unless ($table) { Carp::confess("No table object for $obj" . $obj->id); } } elsif ($obj->isa("UR::DataSource::RDBMS::Table") or $obj->isa("UR::DataSource::RDBMS::Table::Ghost")) { $table = $obj } # we may find no table if it was dropped, and this is one of its old cols/constraints next unless $table; $changed_tables{$table->id} = 1; } # Some ill-behaved modules might set no_commit to true at compile time. # Reset it back to whatever it is now after going through the namespace's modules # Note that when we have class info in the metadata DB, this probably won't be # necessary anymore since we won't have to actually load up the .pm files to # discover classes in the namespace my $remembered_no_commit_setting = UR::DBI->no_commit(); my $remembered_dummy_ids_setting = UR::DataSource->use_dummy_autogenerated_ids(); # # Update the classes based-on changes to the database schemas # ##$DB::single = 1; if (@data_dictionary_objects) { $self->status_message("Found " . keys(%changed_tables) . " tables with changes.") unless $force_rewrite_all_classes; $self->status_message("Resolving corresponding class changes..."); my $success = $self->_update_class_metadata_objects_to_match_database_metadata_changes( data_dictionary_objects => \@data_dictionary_objects ); unless ($success) { return; } } else { $self->status_message("No data schema changes."); } UR::DBI->no_commit($remembered_no_commit_setting); UR::DataSource->use_dummy_autogenerated_ids($remembered_dummy_ids_setting); # # The namespace module may have special rules for creating classes from regular (non-schema) data. # At this point we allow the namespace to adjust the class tree as it chooses. # $namespace->class; if ( $namespace->can("_update_classes_from_data_sources") and not $specified_table_name_arrayref and not $specified_class_name_arrayref and not $specified_data_source_arrayref ) { $self->status_message("Checking for custom changes for the $namespace namespace..."); $namespace->_update_classes_from_data_sources(); } $self->status_message("Saving metadata changes..."); my $sync_success = UR::Context->_sync_databases(); unless ($sync_success) { ##$DB::single = 1; $self->error_message("Metadata sync_database failed"); UR::Context->_rollback_databases(); return; } # # Re-write the class headers for changed classes. # Output a summary report of what has been changed. # This block of logic shold be part of saving class data. # Right now, it's done with a _load() override, no data_source, and this block of code. :( # ##$DB::single = 1; my $cx = UR::Context->current; my @changed_class_meta_objects; my %changed_classes; my $module_update_success = eval { for my $meta_class (qw/ UR::Object::Type UR::Object::Property /) { push @changed_class_meta_objects, grep { $_->__changes__ } $cx->all_objects_loaded($meta_class); my $ghost_class = $meta_class . "::Ghost"; push @changed_class_meta_objects, $cx->all_objects_loaded($ghost_class); } for my $obj ( @changed_class_meta_objects ) { my $class_name = $obj->class_name; next unless $class_name; #if $obj is a ghost, class_name might return undef? $changed_classes{$class_name} = 1; } unless (@changed_class_meta_objects) { $self->status_message("No class changes."); } my $changed_class_count = scalar(keys %changed_classes); my $subj = $changed_class_count == 1 ? "class" : "classes"; $self->status_message("Resolved changes for $changed_class_count $subj"); $self->status_message("Updating the filesystem..."); my $success = $self->_sync_filesystem( changed_class_names => [sort keys %changed_classes], ); return $success; }; if ($@) { $self->error_message("Error updating the filesystem: $@"); return; } elsif (!$module_update_success) { $self->status_message("Error updating filesystem!"); return; } $self->status_message("Filesystem update complete."); # # This commit actually records the data dictionary changes in the ::Meta datasource sqlite database. # $self->status_message("Committing changes to data sources..."); unless (UR::Context->_commit_databases()) { ##$DB::single = 1; $self->error_message("Metadata commit failed"); return; } # # The logic below is only necessary if this process is run as part of some larger process. # Right now that includes the automated test for this module. # After classes have been updated they won't function properly. # Ungenerate and re-generate each of the classes we touched, so that it functions according to its new spec. # $self->status_message("Cleaning up."); my $success = 1; for my $class_name (sort keys %changed_classes) { my $class_obj = UR::Object::Type->get($class_name); next unless $class_obj; $class_obj->ungenerate; Carp::confess("class $class_name didn't ungenerate properly") if $class_obj->generated; unless (eval { $class_obj->generate } ) { $self->warning_message("Class $class_name didn't re-generate properly: $@"); $success = 0; } } unless ($success) { $self->status_message("Errors occurred re-generating some classes after update."); return; } # # Done # $self->status_message("Update complete."); return 1; } # # The execute() method above is broken into three parts: # ->_update_database_metadata_objects_for_schema_changes() # ->_update_class_metadata_objects_to_match_database_metadata_changes() # ->_sync_filesystem() # sub _update_database_metadata_objects_for_schema_changes { my ($self, %params) = @_; my $data_source = delete $params{data_source}; my $force_check_all_tables = delete $params{force_check_all_tables}; my $table_name = delete $params{table_name}; die "unknown params " . Dumper(\%params) if keys %params; #$data_source = $data_source->class; my @changed; my $last_ddl_time_for_table_name = {}; if ($data_source->can("get_table_last_ddl_times_by_table_name") and !$force_check_all_tables) { # the driver implements a way to get the last DDL time $last_ddl_time_for_table_name = $data_source->get_table_last_ddl_times_by_table_name; } # from the cache of known tables my @previous_table_names = $data_source->get_table_names; my %previous_table_names = map { $_ => 1 } @previous_table_names; # from the database now my @current_table_names = $data_source->_get_table_names_from_data_dictionary(); my %current_table_names = map { s/"|'//g; $_ => $_ } @current_table_names; my %all_table_names = $table_name ? ( $table_name => 1 ) : ( %current_table_names, %previous_table_names); my $new_object_revision = $UR::Context::current->now(); # handle tables which are new/updated by updating the class my (@create,@delete,@update); my $pattern = '%-42s'; my ($dsn) = ($data_source->id =~ /^.*::DataSource::(.*?)$/); for my $table_name (keys %all_table_names) { my $last_actual_ddl_time = $last_ddl_time_for_table_name->{$table_name}; my $table_object; my $last_recorded_ddl_time; my $last_object_revision; my $db_table_name = $current_table_names{$table_name}; eval { #($table_object) = $data_source->get_tables(table_name => $table_name); # Using the above doesn't account for a table switching databases, which happens. # Once the data source is _part_ of the id we'll just have a delete/add, but for now it's an update. $table_object = UR::DataSource::RDBMS::Table->get(data_source => $data_source->id, table_name => $table_name); }; if ($current_table_names{$table_name} and not $table_object) { # new table push @create, $table_name; $self->status_message( sprintf( "A $pattern Schema changes " . ($last_actual_ddl_time ? "on $last_actual_ddl_time" : ""), $dsn . " " . $table_name ) ); my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name); next unless $table_object; $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name}); } elsif ($current_table_names{$table_name} and $table_object) { # retained table # either we know it changed, or we can't know, so update it anyway if (! exists $last_ddl_time_for_table_name->{$table_name} or ! defined $table_object->last_ddl_time or $last_ddl_time_for_table_name->{$table_name} gt $table_object->last_ddl_time ) { my $last_update = $table_object->last_ddl_time || $table_object->last_object_revision; my $this_update = $last_ddl_time_for_table_name->{$table_name} || ""; my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name); unless ($table_object) { ##$DB::single = 1; print; } my @changes = # grep { not ($_->properties == 1 and ($_->properties)[0] eq "last_object_revision") } $table_object->__changes__; if (@changes) { $self->status_message( sprintf("U $pattern Last updated on $last_update. Newer schema changes on $this_update." , $dsn . " " . $table_name ) ); push @update, $table_name; } $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name}); } } elsif ($table_object and not $current_table_names{$table_name}) { # deleted table push @delete, $table_name; $self->status_message( sprintf( "D $pattern Last updated on %s. Table dropped.", $dsn . " " . $table_name, $last_object_revision || "" ) ); my $table_object = UR::DataSource::RDBMS::Table->get( data_source => $data_source->id, table_name => $table_name, ); $table_object->delete; } else { Carp::confess("Unable to categorize table $table_name as new/old/deleted?!"); } } return 1; } # Keep a cache of class meta objects so we don't have to keep asking the # object system to do it for us. This should be a speed optimization because # the asking eventually filters down to calling get_material_classes() on the # namespace which can be extremely slow. If it's not in the cache, defer to # asking the data source sub _get_class_meta_for_table_name { my($self,%param) = @_; my $data_source = $param{'data_source'}; my $data_source_name = $data_source->get_name(); my $table_name = $param{'table_name'}; my ($obj) = grep { not $_->isa("UR::Object::Ghost") } UR::Object::Type->is_loaded( data_source_id => $data_source, table_name => $table_name ); return $obj if $obj; unless ($self->{'_class_meta_cache'}{$data_source_name}) { my @classes = grep { not $_->class_name->isa('UR::Object::Ghost') } UR::Object::Type->get(data_source_id => $data_source); for my $class (@classes) { my $table_name = $class->table_name; next unless $table_name; $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name} = $class; } } $obj = $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name}; return $obj if $obj; return; } sub _update_class_metadata_objects_to_match_database_metadata_changes { my ($self, %params) = @_; my $data_dictionary_objects = delete $params{data_dictionary_objects}; if (%params) { $self->error_message("Unknown params!"); return; } # # INITIALIZATION AND SANITY CHECKING # my $namespace = $self->namespace_name; $self->status_message("Updating classes..."); my %dd_changes_by_class = ( 'UR::DataSource::RDBMS::Table' => [], 'UR::DataSource::RDBMS::TableColumn' => [], 'UR::DataSource::RDBMS::FkConstraint' => [], 'UR::DataSource::RDBMS::Table::Ghost' => [], 'UR::DataSource::RDBMS::TableColumn::Ghost' => [], 'UR::DataSource::RDBMS::FkConstraint::Ghost' => [], ); for my $changed_obj (@$data_dictionary_objects) { my $changed_class = $changed_obj->class; my $bucket = $dd_changes_by_class{$changed_class}; push @$bucket, $changed_obj; } my $sorter = sub($$) { no warnings 'uninitialized'; $_[0]->table_name cmp $_[1]->table_name || $_[0]->id cmp $_[1]->id }; # FKs are special, in that they might change names, but we use the name as the "id". # This should change, really, but until it does we need to identify them by their "content", # # DELETIONS # # DELETED FK CONSTRAINTS # Just detach the object reference meta-data from the constraint. # We only actually delete references when their properties all go away, # which can happen when the columns go away (through table deletion or alteration). # It can also happen when one of the involved classes is deleted, which never happens # automatically. for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint::Ghost'} }) { unless ($fk->table_name) { $self->status_message(sprintf("~ No table name for deleted foreign key constraint %-32s\n", $fk->id)); next; } my $table = $fk->get_table; my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table->table_name); unless ($class) { ##$DB::single = 1; $self->status_message(sprintf("~ No class found for deleted foreign key constraint %-32s %-32s\n",$table->table_name, $fk->id)); next; } my $class_name = $class->class_name; my $property = UR::Object::Property->get(class_name => $class_name, constraint_name => $fk->fk_constraint_name); unless ($property) { $self->status_message(sprintf("~ No property found for deleted foreign key constraint %-32s %-32s class $class_name\n", $table->table_name, $fk->fk_constraint_name)); next; } $property->delete; } # DELETED UNIQUE CONSTRAINTS # DELETED PK CONSTRAINTS # We do nothing here, because we don't track these as individual DD objects, just values on the table object. # If a table changes constraints, that is handled below after table/column add/update. # If a table is dropped entirely, we leave all pk/unique constraints in place, # since, if the class is not manually deleted by the developer, it should continue # to function as it did before. # DELETED COLUMNS my @saved_removed_column_messages; # Delete them now, but report about them later in the 'Updating class properties' section for my $column (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::TableColumn::Ghost"} }) { my $table = $column->get_table; unless ($table) { $self->status_message(sprintf("~ No table found for deleted column %-32s\n", $column->id)); next; } my $column_name = $column->column_name; my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table->table_name); unless ($class) { $self->status_message(sprintf("~ No class found for deleted column %-32s %-32s\n", $table->table_name, $column_name)); next; } my $class_name = $class->class_name; my ($property) = $class->direct_property_metas( column_name => $column_name ); unless ($property) { $self->status_message(sprintf("~ No property found for deleted column %-32s %-32s\n",$table->table_name, $column_name)); next; } unless ($table->isa("UR::DataSource::RDBMS::Table::Ghost")) { push(@saved_removed_column_messages, sprintf("D %-40s property %-16s for removed column %s.%s\n", $class->class_name, $property->property_name, $column->table_name, $column->column_name, ) ); } $property->delete; unless ($property->isa("UR::DeletedRef")) { Carp::confess("Error deleting property " . $property->id); } } # DELETED TABLES my %classes_with_deleted_tables; for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table::Ghost"} }) { # Though we create classes for tables, we don't immediately delete them, just deflate them. my $table_name = $table->table_name; unless ($table_name) { $self->status_message("~ No table_name for deleted table object ".$table->id); next; } if (not defined UR::Context->_get_committed_property_value($table,'table_name')) { print Data::Dumper::Dumper($table); ##$DB::single = 1; } # FIXME should this use $data_source->get_class_meta_for_table($table) instead? my $committed_data_source_id = UR::Context->_get_committed_property_value($table,'data_source'); my $committed_table_name = UR::Context->_get_committed_property_value($table,'table_name'); my $class = UR::Object::Type->get( data_source_id => $committed_data_source_id, table_name => $committed_table_name, ); unless ($class) { $self->status_message(sprintf("~ No class found for deleted table %-32s" . "\n",$table->id)); next; } $classes_with_deleted_tables{$table_name} = $class; $class->data_source(undef); $class->table_name(undef); } # next deleted table for my $table_name (keys %classes_with_deleted_tables) { my $class = $classes_with_deleted_tables{$table_name}; my $class_name = $class->class_name; my %ancestory = map { $_ => 1 } $class->inheritance; my @ancestors_with_tables = grep { $a = UR::Object::Type->get(class_name => $_) || UR::Object::Type::Ghost->get(class_name => $_); $a && $a->table_name; } sort keys %ancestory; if (@ancestors_with_tables) { $self->status_message( sprintf("U %-40s class is now detached from deleted table %-32s. It still inherits from classes with persistent storage." . "\n",$class_name,$table_name) ); } else { $self->status_message( sprintf("D %-40s class deleted for deleted table %s" . "\n",$class_name,$table_name) ); } } # next deleted table # This is the data structure used by _get_class_meta_for_table_name # There's a bad interaction with software transactions that can lead # to this cache containing deleted class objects if the caller holds # on to a reference to this command object and repetedly calls execute() # but rolls back transactions between those calls. $self->{'_class_meta_cache'} = {}; ##$DB::single = 1; # # EXISTING DD OBJECTS # # TABLE for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table"} }) { my $table_name = $table->table_name; my $data_source = $table->data_source; my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table_name); if ($class) { # update if ($class->data_source ne $table->data_source) { $class->data_source($table->data_source); } my $class_name = $class->class_name; no warnings; if ($table->remarks ne UR::Context->_get_committed_property_value($table,'remarks')) { $class->doc($table->remarks); } if ($table->data_source ne UR::Context->_get_committed_property_value($table,'data_source')) { $class->data_source($table->data_source); } if ($class->__changes__) { $self->status_message( sprintf("U %-40s class uses %s %s %s" . "\n", $class_name, $table->data_source->get_name, lc($table->table_type), $table_name) ); } } else { # create my $data_source = $table->data_source; my $data_source_id = (ref $data_source ? $data_source->id : $data_source); my $class_name = $data_source->resolve_class_name_for_table_name($table_name,$table->table_type); unless ($class_name) { Carp::confess( "Failed to resolve a class name for new table " . $table_name ); } # if the original table_name was empty (ie. not backed by a table), and the # new one actually has a table, then this is just another schema change and # not an error. Set the table_name attribute and go on... my $class = UR::Object::Type->get(class_name => $class_name); my $prev_table_name = ($class ? $class->table_name : undef); my $prev_data_source_id = ($class ? $class->data_source_id : undef); if ($class && $prev_table_name) { Carp::confess( "Class $class_name already exists for table '$prev_table_name' in $prev_data_source_id." . " Cannot generate class for $table_name in $data_source_id." ); } $self->status_message( sprintf("A %-40s class uses %s %s %s" . "\n", $class_name, $table->data_source->get_name, lc($table->table_type), $table_name) ); if ($class) { $class->doc($table->remarks ? $table->remarks: undef); $class->data_source($data_source); $class->table_name($table_name); $class->er_role($table->er_type); } else { $class = UR::Object::Type->create( class_name => $class_name, doc => ($table->remarks ? $table->remarks: undef), data_source_id => $data_source, table_name => $table_name, er_role => $table->er_type, # generate => 0, ); unless ($class) { Carp::confess( "Failed to create class $class_name for new table " . $table_name . ". " . UR::Object::Type->error_message ); } } } } # next table $self->status_message("Updating direct class properties...\n"); $self->status_message($_) foreach @saved_removed_column_messages; # COLUMN for my $column (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::TableColumn'} }) { my $table = $column->get_table; my $column_name = $column->column_name; my $data_source = $table->data_source; my($ur_data_type, $default_length) = @{ $data_source->ur_data_type_for_data_source_data_type($column->data_type) }; my $ur_data_length = defined($column->data_length) ? $column->data_length : $default_length; my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table->table_name); unless ($class) { $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table->table_name); Carp::confess("Class object missing for table " . $table->table_name) unless $class; } my $class_name = $class->class_name; my $property; foreach my $prop_object ( $class->direct_property_metas ) { if (defined $prop_object->column_name and lc($prop_object->column_name) eq lc($column_name)) { $property = $prop_object; last; } } # We care less whether the column is new/updated, than whether there is property metadata for it. if ($property) { my @column_property_translations = ( # [ column_name, property_name, conversion_sub(column_obj, value) ] ['data_length' => 'data_length', # lengths for these data types are based on the number of bytes used internally in the # database. The UR-based objects will store the text version, which will always be longer, # making $obj->__errors__() complain about the length being out of bounds sub { my ($c, $av) = @_; defined($av) ? $av : ($c->is_time_data ? undef : $ur_data_length) } ], ['data_type' => 'data_type', sub { my ($c, $av) = @_; defined($ur_data_type) ? $ur_data_type : $av } ], ['nullable' => 'is_optional', sub { my ($c, $av) = @_; (defined($av) and ($av eq "Y")) ? 1 : 0 } ], ['remarks' => 'doc', # Ideally this would only use DB value ($av) if the last_ddl_time was newer. sub { my ($c, $av) = @_; defined($av) ? $av : $property->doc } ], ); # update for my $translation (@column_property_translations) { my ($column_attr, $property_attr, $conversion_sub) = @$translation; $property_attr ||= $column_attr; no warnings; if (UR::Context->_get_committed_property_value($column,$column_attr) ne $column->$column_attr) { if ($conversion_sub) { $property->$property_attr($conversion_sub->($column, $column->$column_attr)); } else { $property->$property_attr($column->$column_attr); } } } if ($property->__changes__) { no warnings; $self->status_message( sprintf("U %-40s property %-20s for column %s.%s (%s %s)\n", $class_name, $property->property_name, $table->table_name, $column_name, $column->data_type, $column->data_length) ); } } else { # create my $property_name = $data_source->resolve_property_name_for_column_name($column->column_name); unless ($property_name) { Carp::confess( "Failed to resolve a property name for new column " . $column->column_name ); } my $create_exception; for (my $attempt = 0; $attempt < 3; $attempt++) { $property_name = '_' . $property_name if $attempt; $create_exception = do { local $@; eval { $property = UR::Object::Property->create( class_name => $class_name, property_name => $property_name, column_name => $column_name, data_type => $ur_data_type, data_length => $ur_data_length, is_optional => $column->nullable eq "Y" ? 1 : 0, is_volatile => 0, doc => $column->remarks, is_specified_in_module_header => 1, ); }; $@; }; last if $property; } no warnings 'uninitialized'; $self->status_message( sprintf("A %-40s property %-16s for column %s.%s (%s %s)\n", $class_name, $property->property_name, $table->table_name, $column_name, $column->data_type, $column->data_length) ); unless ($property) { if ($create_exception =~ m/An object of class UR::Object::Property already exists/) { $self->warning_message("Conflicting property names already exist in class $class_name for column $column_name in table ".$table->table_name); } else { Carp::confess( "Failed to create property $property_name on class $class_name. " . UR::Object::Property->error_message ); } } } } # next column $self->status_message("Updating class ID properties...\n"); # PK CONSTRAINTS (loop table objects again, since the DD doesn't do individual ID objects) for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) { # created/updated/unchanged # delete and re-create these objects: they're "bridges", so no developer supplied data is presesent my $table_name = $table->table_name; my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table_name); my $class_name = $class->class_name; my @properties = UR::Object::Property->get(class_name => $class_name); unless (@properties) { $self->warning_message("no properties on class $class_name?"); ##$DB::single = 1; } my @expected_pk_cols = grep { defined } map { $_->column_name } grep { defined $_->is_id } @properties; my @pk_cols = $table->primary_key_constraint_column_names; if ("@expected_pk_cols" eq "@pk_cols") { next; } unless (@pk_cols) { # If there are no primary keys defined, then treat _all_ the columns # as primary keys. This means we don't support multiple rows in a # table containing the same data. @pk_cols = $table->column_names; } my %pk_cols; for my $pos (1 .. @pk_cols) { my $pk_col = $pk_cols[$pos-1]; my ($property) = grep { defined($_->column_name) and ($_->column_name eq $pk_col) } @properties; unless ($property) { # the column has been removed next; } $pk_cols{$property->property_name} = $pos; } # all primary key properties are non-nullable, regardless of what the DB allows for my $property (@properties) { my $name = $property->property_name; if ($pk_cols{$name}) { $property->is_optional(0); $property->is_id($pk_cols{$name}); } } } # next table (looking just for PK constraint changes) # Make another pass to make sure if a class has a property called 'id' with a column attached, # then it must be the only ID property of that class my %classes_to_check_id_properties; foreach my $thing ( qw(UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::TableColumn ) ) { foreach my $item ( @{ $dd_changes_by_class{$thing} } ) { my $class_meta = $self->_get_class_meta_for_table_name(data_source => $item->data_source, table_name => $item->table_name); $classes_to_check_id_properties{$class_meta->class_name} ||= $class_meta; } } foreach my $class_name ( keys %classes_to_check_id_properties ) { my $class_meta = $classes_to_check_id_properties{$class_name}; my $property_meta = $class_meta->property_meta_for_name('id'); if ($property_meta && $property_meta->column_name && scalar($class_meta->direct_id_property_metas) > 1) { $self->warning_message("Class $class_name cannot have multiple ID properties when one concrete ID property is named 'id'. It will likely not function correctly unless it is renamed"); } unless (defined $property_meta->is_id) { $self->warning_message("Class $class_name has a property named 'id' that is not an ID property. It will likely not function correctly unless it is renamed"); } } $self->status_message("Updating class unique constraints...\n"); # UNIQUE CONSTRAINT / UNIQUE INDEX -> UNIQUE GROUP (loop table objecs since we have no PK DD objects) for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) { # created/updated/unchanged # delete and re-create my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table->table_name); my $class_name = $class->class_name; my @properties = UR::Object::Property->get(class_name => $class_name); my @uc_names = $table->unique_constraint_names; for my $uc_name (@uc_names) { eval { $class->remove_unique_constraint($uc_name) }; if ($@ =~ m/There is no constraint named/) { next; # it's OK if there's no UR metadata for this constraint yet } elsif ($@) { die $@; } my @uc_cols = map { ref($_) ? @$_ : $_ } $table->unique_constraint_column_names($uc_name); my @uc_property_names; for my $uc_col (@uc_cols) { my ($property) = grep { defined($_->column_name) and ($_->column_name eq $uc_col) } @properties; unless ($property) { $self->warning_message("No property found for column $uc_col for unique constraint $uc_name"); #$DB::single = 1; next; } push @uc_property_names, $property->property_name; } $class->add_unique_constraint($uc_name, @uc_property_names); } } # next table (checking separately for unique constraints) # FK CONSTRAINTS # These often change name, and as such need to be identified by their actual content. # Each constraint must match some relationship in the system, or a new one will be added. $self->status_message("Updating class relationships...\n"); my $last_class_name = ''; FK: for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint'} }) { my $table = $fk->get_table; my $data_source = $fk->data_source; my $table_name = $fk->table_name; my $r_table_name = $fk->r_table_name; my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table_name); unless ($class) { $self->warning_message( sprintf("No class found for table for foreign key constraint %-32s %s" . "\n",$table_name, $fk->id) ); next; } my $r_class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $r_table_name); unless ($r_class) { $self->warning_message( sprintf("No class found for r_table for foreign key constraint %-32s %-32s" . "\n",$r_table_name, $fk->id) ); next; } my $class_name = $class->class_name; my $r_class_name = $r_class->class_name; # Create an object-accessor property to go with this FK # First we have to figure out a proper delegation name # which is a rather convoluted process my @column_names = $fk->column_names; my @r_column_names = $fk->r_column_names; my (@properties,@property_names,@r_properties,@r_property_names,$prefix,$suffix,$matched); foreach my $i ( 0 .. $#column_names ) { my $column_name = $column_names[$i]; my $property = UR::Object::Property->get( class_name => $class_name, column_name => $column_name, ); unless ($property) { Carp::confess("Failed to find a property for column $column_name on class $class_name"); } push @properties,$property; my $property_name = $property->property_name; push @property_names,$property_name; my $r_column_name = $r_column_names[$i]; my $r_property = UR::Object::Property->get( class_name => $r_class_name, column_name => $r_column_name, ); unless ($r_property) { Carp::cluck("Failed to find a property for column $r_column_name on class $r_class_name"); #$DB::single = 1; next FK; } push @r_properties,$r_property; my $r_property_name = $r_property->property_name; push @r_property_names,$r_property_name; if ($property_name =~ /^(.*)$r_property_name(.*)$/ or $property_name =~ /^(.*)_id$/) { $prefix = $1; $prefix =~ s/_$//g if defined $prefix; $suffix = $2; $suffix =~ s/^_//g if defined $suffix; $matched = 1; } } my @r_class_name_parts = split('::', $r_class->class_name); shift @r_class_name_parts; # drop the namespace name my $delegation_name = lc(join('_', @r_class_name_parts)); if ($matched) { $delegation_name = $delegation_name . "_" . $prefix if $prefix; $delegation_name .= ($suffix !~ /\D/ ? "" : "_") . $suffix if $suffix; } else { $delegation_name = join("_", @property_names) . "_" . $delegation_name; } # Generate a delegation name that dosen't conflict with another already in use my %property_names_used = map { $_ => 1 } $class->all_property_names; while($property_names_used{$delegation_name}) { $delegation_name =~ /^(.*?)(\d*)$/; $delegation_name = $1 . ( ($2 ? $2 : 0) + 1 ); } # FK columns may have been in an odd order. Get the reference columns in ID order. for my $i (0..$#column_names) { my $column_name = $column_names[$i]; my $property = $properties[$i]; my $property_name = $property_names[$i]; my $r_column_name = $r_column_names[$i]; my $r_property = $r_properties[$i]; my $r_property_name = $r_property_names[$i]; } # Pick a name that isn't already a property in that class PICK_A_NAME: for ( 1 ) { if (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name)) { if (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name.'_obj')) { foreach my $i ( 1 .. 10 ) { unless (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name."_$i")) { $delegation_name .= "_$i"; last PICK_A_NAME; } } $self->warning_message("Can't generate a relationship property name for $class_name table name $table_name constraint_name ",$fk->fk_constraint_name); next FK; } else { $delegation_name = $delegation_name.'_obj'; } } } unless ($class->property_meta_for_name($delegation_name)) { my $property = UR::Object::Property->create(class_name => $class_name, property_name => $delegation_name, data_type => $r_class_name, id_by => \@property_names, constraint_name => $fk->fk_constraint_name, is_delegated => 1, is_specified_in_module_header => 1, ); no warnings; $self->status_message( sprintf("A %-40s property %-16s id by %-16s (%s)\n", $class_name, $delegation_name, join(',',@property_names), $r_class_name ) ); } } # next fk constraint return 1; } sub _foreign_key_fingerprint { my($self,$fk) = @_; my $class = $self->_get_class_meta_for_table_name(data_source => $fk->data_source, table_name => $fk->table_name); return $class->class_name . ':' . join(',',sort $fk->column_names) . ':' . join(',',sort $fk->r_column_names); } sub _sync_filesystem { my $self = shift; my %params = @_; my $changed_class_names = delete $params{changed_class_names}; if (%params) { Carp::confess("Invalid params passed to _sync_filesystem: " . join(",", keys %params) . "\n"); } my $obsolete_module_directory = $self->namespace_name->get_deleted_module_directory_name; my $namespace = $self->namespace_name; my $no_commit = UR::DBI->no_commit; $no_commit = 0 if $self->{'_override_no_commit_for_filesystem_items'}; for my $class_name (@$changed_class_names) { my $status_message_this_update = ''; my $class_obj; my $prev; if ($class_obj = UR::Object::Type->get(class_name => $class_name)) { if ($class_obj->{is}[0] =~ /::Type$/ and $class_obj->{is}[0]->isa('UR::Object::Type')) { next; } if ($class_obj->{db_committed}) { $status_message_this_update .= "U " . $class_obj->module_path; } else { $status_message_this_update .= "A " . $class_obj->module_path; } $class_obj->rewrite_module_header() unless ($no_commit); # FIXME A test of automaticly making DBIx::Class modules #$class_obj->dbic_rewrite_module_header() unless ($no_commit); } elsif ($class_obj = UR::Object::Type::Ghost->get(class_name => $class_name)) { if ($class_obj->{is}[0] eq 'UR::Object::Type') { next; } $status_message_this_update = "D " . $class_obj->module_path; unless ($no_commit) { unless (-d $obsolete_module_directory) { mkdir $obsolete_module_directory; unless (-d $obsolete_module_directory) { $self->error_message("Unable to create $obsolete_module_directory for the deleted module for $class_name."); next; } } my $f = IO::File->new($class_obj->module_path); my $old_file_data = join('',$f->getlines); $f->close(); my $old_module_path = $class_obj->module_path; my $new_module_path = $old_module_path; $new_module_path =~ s/\/$namespace\//\/$namespace\/\.deleted\//; $status_message_this_update .= " (moving $old_module_path to $new_module_path)"; rename $old_module_path, $new_module_path; UR::Context::Transaction->log_change($class_obj, $class_obj->class_name, $class_obj->id, 'rewrite_module_header', Data::Dumper::Dumper({path => $new_module_path, data => $old_file_data})); } } else { Carp::confess("Failed to find regular or ghost class meta-object for class $class_name!?"); } if ($no_commit) { $status_message_this_update .= ' (ignored - no-commit)'; } $self->status_message($status_message_this_update); } return 1; } 1; Doc.pm100664023532023421 2365412544604516 20522 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::Doc; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; use File::Basename; use File::Path; use YAML; class UR::Namespace::Command::Update::Doc { is => 'Command::V2', has => [ executable_name => { is => 'Text', shell_args_position => 1, doc => 'the name of the executable to document' }, class_name => { is => 'Text', shell_args_position => 2, doc => 'the command class which maps to the executable' }, targets => { is => 'Text', is_optional => 1, shell_args_position => 3, is_many => 1, doc => 'specific classes to document (documents all unless specified)', }, exclude_sections => { is => 'Text', is_many => 1, is_optional => 1, doc => 'if specified, sections matching these names will be omitted', }, input_path => { is => 'Path', is_optional => 1, doc => 'optional location of the modules to document', }, restrict_to_input_path => { is => 'Boolean', default_value => 1, doc => 'when set, only modules found under the input-path will be processed', }, output_path => { is => 'Text', is_optional => 1, doc => 'optional location to output documentation files', }, output_format => { is => 'Text', default_value => 'pod', valid_values => ['pod', 'html'], doc => 'the output format to write' }, generate_index => { is => 'Boolean', default_value => 1, doc => "when true, an 'index' of all files generated is written (currently works for html only)", }, suppress_errors => { is => 'Boolean', default_value => 1, doc => 'when set, warnings about unloadable modules will not be printed', }, ], has_transient_optional => [ _writer_class => { is => 'Text', }, _index_filename => { is => 'Text', } ], doc => "generate documentation for commands" }; sub help_synopsis { return <<"EOS" ur update doc -i ./lib -o ./doc ur UR::Namespace::Command EOS } sub help_detail { return join("\n", 'This tool generates documentation for each of the commands in a tree for a given executable.', 'This command must be run from within the namespace directory.'); } sub execute { my $self = shift; die "--generate-index requires --output-dir to be specified" if $self->generate_index and !$self->output_path; # scrub any trailing / from input/output_path if ($self->output_path) { my $output_path = $self->output_path; $output_path =~ s/\/+$//m; $self->output_path($output_path); } if ($self->input_path) { my $input_path = $self->input_path; $input_path =~ s/\/+$//m; $self->input_path($input_path); } $self->_writer_class("UR::Doc::Writer::" . ucfirst($self->output_format)); die "Unable to create a writer for output format '" . $self->output_format . "'" unless($self->_writer_class->can("create")); local $ENV{ANSI_COLORS_DISABLED} = 1; my $entry_point_bin = $self->executable_name; my $entry_point_class = $self->class_name; my @targets = $self->targets; unless (@targets) { @targets = ($entry_point_class); } local @INC = @INC; if ($self->input_path) { unshift @INC, $self->input_path; $self->status_message("using modules at " . $self->input_path); } my $errors = 0; for my $target (@targets) { eval "use $target"; if ($@) { $self->error_message("Failed to use $target: $@"); $errors++; } } return if $errors; if ($self->output_path) { unless (-d $self->output_path) { if (-e $self->output_path) { $self->status_message("output path is not a directory!: " . $self->output_path); } else { File::Path::make_path($self->output_path); if (-d $self->output_path) { $self->status_message("using output directory " . $self->output_path); } else { $self->status_message("error creating directory: $! for " . $self->output_path); } } } } local $Command::entry_point_bin = $entry_point_bin; local $Command::entry_point_class = $entry_point_class; my @command_trees = map( $self->_get_command_tree($_), @targets); $self->_generate_index(@command_trees); for my $tree (@command_trees) { $self->_process_command_tree($tree); } return 1; } sub _generate_index { my ($self, @command_trees) = @_; if ($self->generate_index) { my $index = Dump({ command_tree => \@command_trees }); if ($index and $index ne '') { my $index_filename = "index.yml"; my $index_path = join("/", $self->output_path, $index_filename); if (-e $index_path) { $self->warning_message("Index generation overwriting existing file at $index_path"); } my $fh = IO::File->new($index_path, 'w'); unless ($fh) { Carp::croak("Can't open file $index_path for writing: $!"); } $fh->print($index); $fh->close(); $self->_index_filename($index_filename) if -e $index_path; } else { $self->warning_message("Unable to generate index"); } } return; } sub _generate_content { my ($self, $command) = @_; my $doc; eval { my @all_sections = $command->doc_sections; my @sections; for my $s (@all_sections) { push(@sections, $s) unless grep { $s->title =~ /$_/ } $self->exclude_sections; } my $writer = $self->_writer_class->create( sections => \@sections, title => $command->command_name, ); $doc = $writer->render; }; if($@) { $self->warning_message('Could not generate docs for ' . $command . '. ' . $@); return; } unless($doc) { $self->warning_message('No docs generated for ' . $command); return; } my $command_name = $command->command_name; my $filename = $self->_make_filename($command_name); my $dir = $self->_get_output_dir($command_name); my $doc_path = join("/", $dir, $filename); $self->status_message("Writing $doc_path"); my $fh; $fh = IO::File->new('>' . $doc_path) || die "Cannot create file at " . $doc_path . "\n"; print $fh $doc; close($fh); } sub _process_command_tree { my ($self, $tree) = @_; $self->_generate_content($tree->{command}) unless $tree->{external}; for my $subtree (@{$tree->{sub_commands}}) { $self->_process_command_tree($subtree); } } sub _make_filename { my ($self, $class_name) = @_; $class_name =~ s/ /-/g; return "$class_name." . $self->output_format; } sub _get_output_dir { my ($self, $class_name) = @_; return $self->output_path if defined $self->output_path; return File::Basename::dirname($class_name->__meta__->module_path); } sub _navigation_info { my ($self, $cmd_class) = @_; my @navigation_info; if ($cmd_class eq $self->class_name) { push(@navigation_info, [$self->executable_name, undef]); } else { push(@navigation_info, [$cmd_class->command_name_brief, undef]); my $parent_class = $cmd_class->parent_command_class; while ($parent_class) { if ($parent_class eq $self->class_name) { my $uri = $self->_make_filename($self->executable_name); my $name = $self->executable_name; unshift(@navigation_info, [$name, $uri]); last; } else { my $uri = $self->_make_filename($parent_class->command_name); my $name = $parent_class->command_name_brief; unshift(@navigation_info, [$name, $uri]); } $parent_class = $parent_class->parent_command_class; } } if ($self->_index_filename) { unshift(@navigation_info, ["(Top)", $self->_index_filename]); } return @navigation_info; } sub _get_command_tree { my ($self, $command) = @_; my $src = "use $command"; eval $src; if ($@) { $self->error_message("Failed to load class $command: $@") unless $self->suppress_errors; return; } return if $command->_is_hidden_in_docs; my $module_name = $command; $module_name =~ s|::|/|g; $module_name .= '.pm'; my $input_path = $self->input_path ? $self->input_path : ''; my $module_path = $INC{$module_name}; $self->status_message("Loaded $command from $module_name at $module_path"); my $external = $module_path !~ /^$input_path\// ? 1 : 0; my $tree = { command => $command, sub_commands => [], module_path => $module_path, external => $external, parent_class => $command->parent_command_class || undef, description => $command->help_brief, }; if ($command eq $self->class_name) { $tree->{command_name} = $tree->{command_name_brief} = $self->executable_name; } else { $tree->{command_name} = $command->command_name; $tree->{command_name_brief} = $command->command_name_brief; } $tree->{uri} = $self->_make_filename($tree->{command_name}); if ($command->can("sub_command_classes")) { for my $cmd ($command->sub_command_classes) { my $subtree = $self->_get_command_tree($cmd); push(@{$tree->{sub_commands}}, $subtree) if $subtree; } } return $tree; } 1; Pod.pm100664023532023421 1172612544604516 20534 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::Pod; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; class UR::Namespace::Command::Update::Pod { is => 'Command::V2', has => [ executable_name => { is => 'Text', shell_args_position => 1, doc => 'the name of the executable to document' }, class_name => { is => 'Text', shell_args_position => 2, doc => 'the command class which maps to the executable' }, targets => { is => 'Text', shell_args_position => 3, is_many => 1, doc => 'specific classes to document (documents all unless specified)', }, input_path => { is => 'Path', is_optional => 1, doc => 'optional location of the modules to document', }, output_path => { is => 'Text', is_optional => 1, doc => 'optional location to output .pod files', }, ], doc => "generate man-page-like POD for a commands" }; sub help_synopsis { return <<"EOS" ur update pod -i ./lib -o ./pod ur UR::Namespace::Command EOS } sub help_detail { return join("\n", 'This tool generates POD documentation for each all of the commands in a tree for a given executable.', 'This command must be run from within the namespace directory.'); } sub execute { my $self = shift; #$DB::single = 1; local $ENV{ANSI_COLORS_DISABLED} = 1; my $entry_point_bin = $self->executable_name; my $entry_point_class = $self->class_name; my @targets = $self->targets; unless (@targets) { @targets = ($entry_point_class); } local @INC = @INC; if ($self->input_path) { unshift @INC, $self->input_path; $self->status_message("using modules at " . $self->input_path); } my $errors = 0; for my $target (@targets) { eval "use $target"; if ($@) { $self->error_message("Failed to use $target: $@"); $errors++; } } return if $errors; my @commands = map( $self->get_all_subcommands($_), @targets); push @commands, @targets; if ($self->output_path) { unless (-d $self->output_path) { if (-e $self->output_path) { $self->status_message("output path is not a directory!: " . $self->output_path); } else { mkdir $self->output_path; if (-d $self->output_path) { $self->status_message("using output directory " . $self->output_path); } else { $self->status_message("error creating directory: $! for " . $self->output_path); } } } } local $Command::V1::entry_point_bin = $entry_point_bin; local $Command::V2::entry_point_bin = $entry_point_bin; local $Command::V1::entry_point_class = $entry_point_class; local $Command::V2::entry_point_class = $entry_point_class; for my $command (@commands) { my $pod; eval { $pod = $command->help_usage_command_pod(); }; if($@) { $self->warning_message('Could not generate POD for ' . $command . '. ' . $@); next; } unless($pod) { $self->warning_message('No POD generated for ' . $command); next; } my $pod_path; if (defined $self->output_path) { my $filename = $command->command_name . '.pod'; $filename =~ s/ /-/g; my $output_path = $self->output_path; $output_path =~ s|/+$||m; $pod_path = join('/', $output_path, $filename); } else { $pod_path = $command->__meta__->module_path; $pod_path =~ s/.pm/.pod/; } $self->status_message("Writing $pod_path"); my $fh; $fh = IO::File->new('>' . $pod_path) || die "Cannot create file at " . $pod_path . "\n"; print $fh $pod; close($fh); } return 1; } sub get_all_subcommands { my $self = shift; my $command = shift; my $src = "use $command"; eval $src; if ($@) { $self->error_message("Failed to load class $command: $@"); } else { my $module_name = $command; $module_name =~ s|::|/|g; $module_name .= '.pm'; $self->status_message("Loaded $command from $module_name at $INC{$module_name}\n"); } my @subcommands; eval { if ($command->can('sub_command_classes')) { @subcommands = $command->sub_command_classes; } }; if($@) { $self->warning_message("Error getting subclasses for module $command: " . $@); } return unless @subcommands and $subcommands[0]; #Sometimes sub_command_classes returns 0 instead of the empty list return map($self->get_all_subcommands($_), @subcommands), @subcommands; } 1; RenameClass.pm100664023532023421 434712544604516 22170 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::RenameClass; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Update::RewriteClassHeader", ); # Standard methods used to output help. sub shell_args_description { "My::Old My::New"; } sub help_description { "Updates class descriptions for to correspond with database schema changes." } # Wrap execute since we take different parameters than a standard # "runs on modules in tree" rewrite command. our $old; our $new; sub execute { my $self = shift; $old = shift; $new = shift; $self->error_message("rename $old to $new not implemented"); return; } # Override "before" to do the class editing. sub before { my $self = shift; my $class_objects = shift; # By default, no classes are rewritten. # As we find classes with the $old name, in their metadata, # we add them to this list. $class_objects = []; print "finding properties which seem to refernce a class\n"; my @p = UR::Object::Property->is_loaded( property_name => ["class_name","r_class_name","parent_class_name"] ); print "found " . join("\n",map { $_->class_name . " -> " . $_->property_name } @_) . "\n"; print "checking instances of those properties\n"; my @changes; for my $p (@p) { my $class_name = $p->class_name; my $property_name = $p->property_name; my @obj = $class_name->is_loaded(); for my $obj (@obj) { if ($obj->$property_name eq $new) { Carp::confess("Name $new is already in use on $class_name, " . $obj->{id} . $property_name . "!" ); } elsif ($obj->$property_name eq $old) { print "Setting $new in place of $old on $class_name, " . $obj->{id} . $property_name . ".\n"; push @changes, [$obj,$property_name,$new]; } } } return 1; } # we implement before() but use the default call to # for_each_class_object() call in UR::Namespace::Command::rewrite 1; RewriteClassHeader.pm100664023532023421 311512544604516 23503 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::RewriteClassHeader; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ force => { is => 'Boolean', is_optional => 1 }, ] ); sub params_as_getopt_specification { my $self = shift; my @spec = $self->SUPER::params_as_getopt_specification(@_); return (@spec, "force!"); } sub help_brief { "Update::RewriteClassHeaders class descriptions headers to normalize manual changes." } sub help_detail { qq| UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. | } sub for_each_class_object { #$DB::single = 1; my $self = shift; my $class = shift; my $old = $class->module_header_source; my $new = $class->resolve_module_header_source; if ($self->force or ($old ne $new)) { print "Updating:\t", $class->module_base_name, "\n"; $class->rewrite_module_header and return 1; print STDERR "Error rewriting header!" and return 0; } else { #print $class->class_name . " has no source changes. " # . "Ignoring " . $class->module_base_name . ".\n"; return 1; } } 1; #$Header$ SchemaDiagram.pm100664023532023421 2426012544604516 22474 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::SchemaDiagram; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ data_source => {type => 'String', doc => 'Which datasource to use', is_optional => 1}, depth => { type => 'Integer', doc => 'Max distance of related tables to include. Default is 1. 0 means show only the named tables, -1 means to include everything', is_optional => 1}, file => { type => 'String', doc => 'Pathname of the Umlet (.uxf) file' }, show_columns => { type => 'Boolean', is_optional => 1, default => 1, doc => 'Include column names in the diagram' }, initial_name => { is_many => 1, is_optional => 1, shell_args_position => 1 } ], ); sub sub_command_sort_position { 3 }; sub help_brief { "Update an Umlet diagram based on the current schema" } sub help_detail { return < 1000; # FIXME This execute() and the one from ur update class-diagram should be combined since they share # most of the code sub execute { my $self = shift; my $params = shift; #$DB::single = 1; my $namespace = $self->namespace_name; eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } # FIXME this is a workaround for a bug. If you try to get Table objects filtered by namespace, # you have to have already instantiated the namespace's data source objects into the object cache # first map { $_->_singleton_object } $namespace->get_data_sources; my @initial_name_list; if ($params->{'depth'} == -1) { # They wanted them all... Ignore whatever is on the command line @initial_name_list = map { $_->table_name} UR::DataSource::RDBMS::Table->get(namespace => $namespace); } else { @initial_name_list = $self->initial_name; } my $diagram; if (-f $params->{'file'}) { $params->{'depth'} = 0 unless (exists $params->{'depth'}); # Default is just update what's there $diagram = UR::Object::Umlet::Diagram->create_from_file($params->{'file'}); push @initial_name_list, map { $_->subject_id } UR::Object::Umlet::Class->get(diagram_name => $diagram->name); } else { $params->{'depth'} = 1 unless exists($params->{'depth'}); $diagram = UR::Object::Umlet::Diagram->create(name => $params->{'file'}); } # FIXME this can get removed when attribute defaults work correctly unless (exists $params->{'show_attributes'}) { $self->show_columns(1); } my @involved_tables = map { UR::DataSource::RDBMS::Table->get(table_name => $_, namespace => $namespace) } @initial_name_list; #foreach my $table_name ( @initial_name_list ) { # # FIXME namespace dosen't work here either # push @involved_tables, UR::DataSource::RDBMS::Table->get(namespace => $namespace, table_name => $table_name); #} #$DB::single = 1; push @involved_tables ,$self->_get_related_items( names => \@initial_name_list, depth => $params->{'depth'}, namespace => $namespace, item_class => 'UR::DataSource::RDBMS::Table', item_param => 'table_name', related_class => 'UR::DataSource::RDBMS::FkConstraint', related_param => 'r_table_name', ); my %involved_table_names = map { $_->table_name => 1 } @involved_tables; # Figure out the initial placement # The initial placement, and how much to move over for the next box my($x_coord, $y_coord, $x_inc, $y_inc) = (20,20,40,40); my @objs = sort { $b->y <=> $a->y or $b->x <=> $a->x } UR::Object::Umlet::Class->get(); if (@objs) { my $maxobj = $objs[0]; $x_coord = $maxobj->x + $maxobj->width + $x_inc; $y_coord = $maxobj->y + $maxobj->height + $y_inc; } # First, place all the tables' boxes my @all_boxes = UR::Object::Umlet::Class->get( diagram_name => $diagram->name ); foreach my $table ( @involved_tables ) { my $umlet_table = UR::Object::Umlet::Class->get(diagram_name => $diagram->name, subject_id => $table->table_name); my $created = 0; unless ($umlet_table) { $created = 1; $umlet_table = UR::Object::Umlet::Class->create( diagram_name => $diagram->name, subject_id => $table->table_name, label => $table->table_name, x => $x_coord, y => $y_coord, ); if ($self->show_columns) { my $attributes = $umlet_table->attributes || []; my %attributes_already_in_diagram = map { $_->{'name'} => 1 } @{ $attributes }; my %pk_properties = map { $_ => 1 } $table->primary_key_constraint_column_names; my $line_count = scalar @$attributes; foreach my $column_name ( $table->column_names ) { next if $attributes_already_in_diagram{$column_name}; $line_count++; my $column = UR::DataSource::RDBMS::TableColumn->get(table_name => $table->table_name, column_name => $column_name, namespace => $namespace); push @$attributes, { is_id => $pk_properties{$column_name} ? '+' : ' ', name => $column_name, type => $column->data_type, line => $line_count, }; } $umlet_table->attributes($attributes); } # Make sure this box dosen't overlap other boxes while(my $overlapped = $umlet_table->is_overlapping(@all_boxes) ) { if ($umlet_table->x > MAX_X_AUTO_POSITION) { $umlet_table->x(20); $umlet_table->y( $umlet_table->y + $y_inc); } else { $umlet_table->x( $overlapped->x + $overlapped->width + $x_inc ); } } push @all_boxes, $umlet_table; } if ($created) { $x_coord = $umlet_table->x + $umlet_table->width + $x_inc; if ($x_coord > MAX_X_AUTO_POSITION) { $x_coord = 20; $y_coord += $y_inc; } } } # Next, connect the tables together foreach my $table ( @involved_tables ) { foreach my $fk ( UR::DataSource::RDBMS::FkConstraint->get(table_name => $table->table_name, namespace => $namespace) ) { next unless ($involved_table_names{$fk->r_table_name}); my $umlet_relation = UR::Object::Umlet::Relation->get( #diagram_name => $diagram->name, from_entity_name => $fk->table_name, to_entity_name => $fk->r_table_name, ); unless ($umlet_relation) { my @fk_column_names = $fk->column_name_map(); my $label = join("\n", map { $_->[0] . " -> " . $_->[1] } @fk_column_names); $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<-', label => $label, from_entity_name => $fk->table_name, to_entity_name => $fk->r_table_name, ); $umlet_relation->connect_entities(); } } } $diagram->save_to_file($params->{'file'}); 1; } sub _get_related_items { my($self, %params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; my $item_class = $params{'item_class'}; my $item_param = $params{'item_param'}; my $related_class = $params{'related_class'}; my $related_param = $params{'related_param'}; # Get everything linked to the named things my @related_names = map { $_->$related_param } $related_class->get($item_param => $params{'names'}, namespace => $params{'namespace'}); push @related_names, map { $_->$item_param } $related_class->get($related_param => $params{'names'}, namespace => $params{'namespace'}); return unless @related_names; my @objs = $item_class->get($item_param => \@related_names, namespace => $params{'namespace'}); # make a recursive call to get the related objects by name return ( @objs, $self->_get_related_items( %params, names => \@related_names, depth => --$params{'depth'}) ); } 1; TabCompletionSpec.pm100664023532023421 651312544604516 23343 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::TabCompletionSpec; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; use POSIX qw(ENOENT); UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ classname => { is => 'Text', shell_args_position => 1, doc => 'The base class to use as trunk of command tree, e.g. UR::Namespace::Command', }, output => { is => 'Text', is_optional => 1, doc => 'Override output location of the opts spec file.', }, ] ); sub help_brief { "Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command."; } sub create { my $class = shift; my $bx = $class->define_boolexpr(@_); if($bx->specifies_value_for('classname') and !$bx->specifies_value_for('namespace_name')) { my $classname = $bx->value_for('classname'); my($namespace) = ($classname =~ m/^(\w+)::/); $bx = $bx->add_filter(namespace_name => $namespace) if $namespace; } return $class->SUPER::create($bx); } sub is_sub_command_delegator { 0; } sub execute { my $self = shift; my $class = $self->classname; my $req_exception = do { local $@; eval { require Getopt::Complete; require Getopt::Complete::Cache; }; $@; }; if ($req_exception) { die "Errors using Getopt::Complete. Do you have Getopt::Complete installed? If not try 'cpanm Getopt::Complete'"; } my $use_exception = do { local $@; eval "use above '$class';"; $@; }; if ($use_exception) { $self->error_message("Unable to use above $class.\n$@"); return; } (my $module_path) = Getopt::Complete::Cache->module_and_cache_paths_for_package($class, 1); my $cache_path = $module_path . ".opts"; eval { my $rename_ok = rename($cache_path, "$cache_path.bak"); if (!$rename_ok && $! != ENOENT) { die "failed to rename file: $!: $cache_path"; } unless ($self->output) { $self->output($cache_path); } $self->status_message("Generating " . $self->output . " file for $class."); $self->status_message("This may take some time and may generate harmless warnings..."); my $fh = IO::File->new($self->output, 'w') or die "Cannot create file at " . $self->output . "\n"; my $src = Data::Dumper::Dumper($class->resolve_option_completion_spec()); $src =~ s/^\$VAR1/\$$class\:\:OPTS_SPEC/; $fh->print($src); }; if (-s $cache_path) { $self->status_message("\nOPTS_SPEC file created at $cache_path"); unlink("$cache_path.bak") or $self->error_message("failed to remove backup file: $!"); } else { if (-s "$cache_path.bak") { $self->error_message("$cache_path is 0 bytes, reverting to previous"); rename("$cache_path.bak", $cache_path) or $self->error_message("failed to restore file: $!"); } else { $self->error_message("$cache_path is 0 bytes and no backup exists, removing file"); unlink($cache_path) or $self->error_message("failed to remove file: $!"); } } } 1; Object.pm100664023532023421 13664412544604516 14553 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Object; use warnings; use strict; require UR; use Scalar::Util qw(looks_like_number); our @ISA = ('UR::ModuleBase'); our $VERSION = "0.44"; # UR $VERSION;; # Base object API sub class { ref($_[0]) || $_[0] } sub id { $_[0]->{id} } sub create { $UR::Context::current->create_entity(@_); } sub get { $UR::Context::current->query(@_); } sub delete { $UR::Context::current->delete_entity(@_); } sub copy { my $self = shift; my %override = @_; my $meta = $self->__meta__; my @copyable_properties = grep { !$_->is_delegated && !$_->is_id } $meta->properties; my %params; for my $p (@copyable_properties) { my $name = $p->property_name; if ($p->is_many) { if (my @value = $self->$name) { $params{$name} = \@value; } } else { if (defined(my $value = $self->$name)) { $params{$name} = $value; } } } return $self->class->create(%params, %override); } # Meta API sub __context__ { # In UR, a "context" handles inter-object references so they can cross # process boundaries, and interact with persistance systems automatically. # For efficiency, all context switches update a package-level value. # We will ultimately need to support objects recording their context explicitly # for things such as data maintenance operations. This shouldn't happen # during "business logic". return $UR::Context::current; } sub __meta__ { # the class meta object # subclasses set this specifically for efficiency upon construction # the base class has a generic implementation for boostrapping Carp::cluck("using the default __meta__!"); my $class_name = shift; return $UR::Context::all_objects_loaded->{"UR::Object::Type"}{$class_name}; } # The identity operation. Not particularly useful by itself, but makes # things like mapping operations easier and calculate_from metadata able # to include the object as function args to calculated properties sub __self__ { return $_[0] if @_ == 1; my $self = shift; my $bx = $self->class->define_boolexpr(@_); if ($bx->evaluate($self)) { return $self; } else { return; } } # Used to traverse n levels of indirect properties, even if the total # indirection is not defined on the primary ofhect this is called on. # For example: $obj->__get_attr__('a.b.c'); # gets $obj's 'a' value, calls 'b' on that, and calls 'c' on the last thing sub __get_attr__ { my ($self, $property_name) = @_; my @property_values; if (index($property_name,'.') == -1) { @property_values = $self->$property_name; } else { my @links = split(/\./,$property_name); @property_values = ($self); for my $full_link (@links) { my $pos = index($full_link,'-'); my $link = ($pos == -1 ? $full_link : substr($full_link,0,$pos) ); @property_values = map { defined($_) ? $_->$link : undef } @property_values; } } return if not defined wantarray; return @property_values if wantarray; if (@property_values > 1) { my $class_name = $self->__meta__->class_name; Carp::confess("Multiple values returned for $class_name $property_name in scalar context!"); } return $property_values[0]; } sub __label_name__ { # override to provide default labeling of the object my $self = $_[0]; my $class = ref($self) || $self; my ($label) = ($class =~ /([^:]+)$/); $label =~ s/([a-z])([A-Z])/$1 $2/g; $label =~ s/([A-Z])([A-Z]([a-z]|\s|$))/$1 $2/g; $label = uc($label) if $label =~ /_id$/i; return $label; } sub __display_name__ { # default stringification (does override "" unless you specifically choose to) my $self = shift; my $in_context_of_related_object = shift; my $name = $self->id; $name =~ s/\t/ /g; return $name; if (not $in_context_of_related_object) { # no in_context_of_related_object. # the object is identified globally return $self->label_name . ' ' . $name; } elsif ($in_context_of_related_object eq ref($self)) { # the class is completely known # show only the core display name # -> less text, more in_context_of_related_object return $name } else { # some intermediate base class is known, # TODO: make this smarter # For now, just show the whole class name with the ID return $self->label_name . ' ' . $name; } } sub __errors__ { # This is the basis for software constraint checking. # Return a list of values describing the problems on the object. my ($self,@property_names) = @_; my $class_object = $self->__meta__; unless (scalar @property_names) { @property_names = $class_object->all_property_names; } my @properties = map { $class_object->property_meta_for_name($_); } @property_names; my @tags; for my $property_metadata (@properties) { # For now we don't validate these. # Ultimately, we should delegate to the property metadata object for value validation. my($is_delegated, $is_calculated, $property_name, $is_optional, $generic_data_type, $data_length) = @$property_metadata{'is_delegated','is_calculated','property_name','is_optional', 'data_type','data_length'}; next if $is_delegated || $is_calculated; # TODO: is this making commits slow by calling lots of indirect accessors? my @values = $self->$property_name; next if @values > 1; my $value = $values[0]; # account for minus sign in dummy ID if ($ENV{UR_USE_DUMMY_AUTOGENERATED_IDS} and $property_metadata->is_id and defined($value) and index($value, '-') == 0 and defined $data_length) { $data_length++; } if (! ($is_optional or defined($value))) { push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => "No value specified for required property", ); } # The tests below don't apply do undefined values. # Save the trouble and move on. next unless defined $value; # Check data type # TODO: delegate to the data type module for this $generic_data_type = '' unless (defined $generic_data_type); if ($generic_data_type eq 'Float' || $generic_data_type eq 'Integer') { if (looks_like_number($value)) { $value = $value + 0; } else{ push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => "Invalid $generic_data_type value." ); } } elsif ($generic_data_type eq 'DateTime') { # This check is currently disabled b/c of time format irrecularities # We rely on underlying database constraints for real invalidity checking. # TODO: fix me if (1) { } elsif ($value =~ /^\s*\d\d\d\d\-\d\d-\d\d\s*(\d\d:\d\d:\d\d|)\s*$/) { # TODO more validation here for a real date. } else { push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => 'Invalid date string.' ); } } # Check size if ($generic_data_type ne 'DateTime') { if ( defined($data_length) and ($data_length < length($value)) ) { push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => sprintf('Value too long (%s of %s has length of %d and should be <= %d).', $property_name, $self->$property_name, length($value), $data_length) ); } } # Check valid values if there is an explicit list if (my $constraints = $property_metadata->valid_values) { my $valid = 0; for my $valid_value (@$constraints) { no warnings; # undef == '' if ($value eq $valid_value) { $valid = 1; last; } } unless ($valid) { # undef is a valid value in the constraints list my $value_list = join(', ',map { defined($_) ? $_ : '' } @$constraints); push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => sprintf( 'The value %s is not in the list of valid values for %s. Valid values are: %s', $value, $property_name, $value_list ) ); } } # Check FK if it is easy to do. # TODO: This is a heavy weight check, and is disabled for performance reasons. # Ideally we'd check a foreign key value _if_ it was changed only, since # saved foreign keys presumably could not have been save if they were invalid. if (0) { my $r_class; unless ($r_class->get(id => $value)) { push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => "$value does not reference a valid " . $r_class . '.' ); } } } return @tags; } # Standard API for working with UR fixtures # boolean expressions # sets # iterators # views # mock objects sub define_boolexpr { if (ref($_[0])) { my $class = ref(shift); return UR::BoolExpr->resolve($class,@_); } else { return UR::BoolExpr->resolve(@_); } } sub define_set { my $class = shift; $class = ref($class) || $class; my $rule = UR::BoolExpr->resolve_normalized($class,@_); my $flattened_rule = $rule->flatten_hard_refs(); my $set_class = $class . "::Set"; return $set_class->get($flattened_rule->id); } sub add_observer { my $self = shift; my %params = @_; if (ref($self)) { $params{subject_id} = $self->id; } my $observer = UR::Observer->create( subject_class_name => $self->class, %params, ); unless ($observer) { $self->error_message( "Failed to create observer: " . UR::Observer->error_message ); return; } return $observer; } sub remove_observers { my $self = shift; my %params = @_; my $aspect = delete $params{'aspect'}; my $callback = delete $params{'callback'}; if (%params) { Carp::croak('Unrecognized parameters for observer removal: ' . Data::Dumper::Dumper(\%params) . "Expected 'aspect' and 'callback'"); } my %args = ( subject_class_name => $self->class ); $args{'subject_id'} = $self->id if (ref $self); $args{'aspect'} = $aspect if (defined $aspect); $args{'callback'} = $callback if (defined $callback); my @observers = UR::Observer->get(%args); $_->delete foreach @observers; return @observers; } sub create_iterator { my $class = shift; # old syntax = create_iterator(where => [param_a => A, param_b => B]) if (@_ > 1) { my %params = @_; if (exists $params{'where'}) { Carp::carp('create_iterator called with old syntax create_iterator(where => \@params) should be called as create_iterator(@params)'); @_ = $params{'where'}; } } # new syntax, same as get() = create_iterator($bx) or create_iterator(param_a => A, param_b => B) my $filter; if (Scalar::Util::blessed($_[0]) && $_[0]->isa('UR::BoolExpr')) { $filter = $_[0]; } else { $filter = UR::BoolExpr->resolve($class, @_) } my $iterator = UR::Object::Iterator->create_for_filter_rule($filter); unless ($iterator) { $class->error_message(UR::Object::Iterator->error_message); return; } return $iterator; } sub create_view { my $self = shift; my $class = $self->class; # this will auto-subclass into ${class}::View::${perspective}::${toolkit}, # using $class or some parent class of $class my $view = UR::Object::View->create( subject_class_name => $class, perspective => "default", @_ ); unless ($view) { $self->error_message("Error creating view: " . UR::Object::View->error_message); return; } if (ref($self)) { $view->subject($self); } return $view; } sub create_mock { my $class = shift; my %params = @_; require Test::MockObject; my $self = Test::MockObject->new(); my $subject_class_object = $class->__meta__; for my $class_object ($subject_class_object,$subject_class_object->ancestry_class_metas) { for my $property ($class_object->direct_property_metas) { my $property_name = $property->property_name; if (($property->is_delegated || $property->is_optional) && !exists($params{$property_name})) { next; } if ($property->is_mutable || $property->is_calculated || $property->is_delegated) { my $sub = sub { my $self = shift; if (@_) { if ($property->is_many) { $self->{'_'. $property_name} = @_; } else { $self->{'_'. $property_name} = shift; } } return $self->{'_'. $property_name}; }; $self->mock($property_name, $sub); if ($property->is_optional) { if (exists($params{$property_name})) { $self->$property_name($params{$property_name}); } } else { unless (exists($params{$property_name})) { if (defined($property->default_value)) { $params{$property_name} = $property->default_value; } else { unless ($property->is_calculated) { Carp::croak("Failed to provide value for required mutable property '$property_name'"); } } } $self->$property_name($params{$property_name}); } } else { unless (exists($params{$property_name})) { if (defined($property->default_value)) { $params{$property_name} = $property->default_value; } else { Carp::croak("Failed to provide value for required property '$property_name'"); } } if ($property->is_many) { $self->set_list($property_name,$params{$property_name}); } else { $self->set_always($property_name,$params{$property_name}); } } } } my @classes = ($class, $subject_class_object->ancestry_class_names); $self->set_isa(@classes); $UR::Context::all_objects_loaded->{$class}->{$self->id} = $self; return $self; } # Typically only used internally by UR except when debugging. sub __changes__ { # Return a list of changes present on the object _directly_. # This is really only useful internally because the boundary of the object # is internal/subjective. my $self = shift; # performance optimization return unless $self->{_change_count}; my $meta = $self->__meta__; if (ref($meta) eq 'UR::DeletedRef') { print Data::Dumper::Dumper($self,$meta); Carp::confess("Meta is deleted for object requesting changes: $self\n"); } if (!$meta->is_transactional and !$meta->is_meta_meta) { return; } my $orig = $self->{db_saved_uncommitted} || $self->{db_committed}; my %prop_metas; my $prop_is_changed = sub { my $prop_name = shift; my $property_meta = $prop_metas{$prop_name} ||= $meta->property_meta_for_name($prop_name); no warnings 'uninitialized'; return ($orig->{$prop_name} ne $self->{$prop_name}) && ($self->can($prop_name) and ! UR::Object->can($prop_name)) && defined($property_meta) && (! $property_meta->is_transient) ; }; unless (wantarray) { # scalar context only cares if there are any changes or not if (@_) { foreach (@_) { return 1 if $prop_is_changed->($_); } return ''; } else { return ($self->{__defined} and $self->{_change_count} == 1) ? '' : $self->{_change_count}; } } no warnings; my @changed; if ($orig) { my $class_name = $meta->class_name; @changed = grep { $prop_is_changed->($_) } grep { $_ } @_ ? (@_) : keys(%$orig); } else { @changed = $meta->all_property_names } return map { UR::Object::Tag->create ( type => 'changed', properties => [$_] ) } @changed; } sub _changed_property_names { my $self = shift; my @changes = $self->__changes__; my %changed_properties; foreach my $change ( @changes ) { next unless ($change->type eq 'changed'); $changed_properties{$_} = 1 foreach $change->properties; } return keys %changed_properties; } sub __signal_change__ { # all mutable property accessors ("setters") call this method to tell the # current context about a state change. $UR::Context::current->add_change_to_transaction_log(@_); $UR::Context::current->send_notification_to_observers(@_); } # send notifications that aren't state changes to observers sub __signal_observers__ { $UR::Context::current->send_notification_to_observers(@_); } sub __define__ { # This is used internally to "virtually load" things. # Simply assert they already existed externally, and act as though they were just loaded... # It is used for classes defined in the source code (which is the default) by the "class {}" magic # instead of in some database, as we'd do for regular objects. It is also used by some test cases. if ($UR::initialized and $_[0] ne 'UR::Object::Property') { # the nornal implementation has all create() features my $self; do { local $UR::Context::construction_method = '__define__'; $self = $UR::Context::current->create_entity(@_); }; return unless $self; $self->{db_committed} = { %$self }; $self->{'__defined'} = 1; $self->__signal_change__("load"); return $self; } else { # used during boostrapping my $class = shift; my $class_meta = $class->__meta__; if (my $method_name = $class_meta->sub_classification_method_name) { my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, @_); my $sub_class_name = $class->$method_name(@_); if ($sub_class_name ne $class) { # delegate to the sub-class to create the object return $sub_class_name->__define__(@_); } } my $self = $UR::Context::current->_construct_object($class, @_); return unless $self; $self->{db_committed} = { %$self }; $self->__signal_change__("load"); return $self; } } sub __extend_namespace__ { # A class Foo can implement this method to have a chance to auto-define Foo::Bar # TODO: make a Class::Autouse::ExtendNamespace Foo => sub { } to handle this. # Right now, UR::ModuleLoader will try it after "use". my $class = shift; my $ext = shift; my $class_meta = $class->__meta__; return $class_meta->generate_support_class_for_extension($ext); } # Handling of references within the current process sub is_weakened { my $self = shift; return (exists $self->{__weakened} && $self->{__weakened}); } sub __weaken__ { # Mark this object as unloadable by the object cache pruner. # If the class has a data source, then a weakened object is dropped # at the first opportunity, reguardless of its __get_serial number. # For classes without a data source, then it will be dropped according to # the normal rules w/r/t the __get_serial (classes without data sources # normally are never dropped by the pruner) my $self = $_[0]; delete $self->{'__strengthened'}; $self->{'__weakened'} = 1; } sub is_strengthened { my $self = shift; return (exists $self->{__strengthened} && $self->{__strengthened}); } sub __strengthen__ { # Indicate this object should never be unloaded by the object cache pruner # or AutoUnloadPool my $self = $_[0]; delete $self->{'__weakened'}; $self->{'__strengthened'} = 1; } sub is_prunable { my $self = shift; return 0 if $self->is_strengthened; return 1 if $self->is_weakened; return 0 if $self->__meta__->is_meta; return 0 if $self->{__get_serial} && $self->__changes__ && @{[$self->__changes__]}; return 1; } sub __rollback__ { my $self = shift; my $saved = $self->{db_saved_uncommitted} || $self->{db_committed}; unless ($saved) { return UR::Object::delete($self); } my $meta = $self->__meta__; my $should_rollback = sub { my $property_meta = shift; return ! ( defined $property_meta->is_id || ! defined $property_meta->column_name || $property_meta->is_delegated || $property_meta->is_legacy_eav || ! $property_meta->is_mutable || $property_meta->is_transient || $property_meta->is_constant ); }; my @rollback_property_names = map { $_->property_name } grep { $should_rollback->($_) } map { $meta->property_meta_for_name($_) } $meta->all_property_names; # Existing object. Undo all changes since last sync, or since load # occurred when there have been no syncs. foreach my $property_name ( @rollback_property_names ) { $self->__rollback_property__($property_name); } delete $self->{'_change_count'}; return $self; } sub __rollback_property__ { my ($self, $property_name) = @_; my $saved = $self->{db_saved_uncommitted} || $self->{db_committed}; unless ($saved) { Carp::croak(qq(Cannot rollback property '$property_name' because it has no saved state)); } my $saved_value = UR::Context->current->value_for_object_property_in_underlying_context($self, $property_name); return $self->$property_name($saved_value); } sub DESTROY { # Handle weak references in the object cache. my $obj = shift; # $destroy_should_clean_up_all_objects_loaded will be true if either light_cache is on, or # the cache_size_highwater mark is a valid value if ($UR::Context::destroy_should_clean_up_all_objects_loaded) { my $class = ref($obj); my $obj_from_cache = delete $UR::Context::all_objects_loaded->{$class}{$obj->{id}}; if ($obj->__meta__->is_meta_meta or @{[$obj->__changes__]}) { die "Object found in all_objects_loaded does not match destroyed ref/id! $obj/$obj->{id}!" unless $obj eq $obj_from_cache; $UR::Context::all_objects_loaded->{$class}{$obj->{id}} = $obj; print "KEEPING $obj. Found $obj .\n"; return; } else { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY object $obj class ",$obj->class," id ",$obj->id,"\n"; } $obj->unload(); return $obj->SUPER::DESTROY(); } } else { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY object $obj class ",$obj->class," id ",$obj->id,"\n"; } $obj->SUPER::DESTROY(); } }; END { # Turn off monitoring of the DESTROY handler at application exit. # setting the typeglob to undef does not work. -sms delete $UR::Object::{DESTROY}; }; # This module implements the deprecated parts of the UR::Object API require UR::ObjectDeprecated; 1; =pod =head1 NAME UR::Object - transactional, queryable, process-independent entities =head1 SYNOPSIS Create a new object in the current context, and return it: $elmo = Acme::Puppet->create( name => 'Elmo', father => $ernie, mother => $bigbird, jobs => [$dance, $sing], favorite_color => 'red', ); Plain accessors work in the typial fashion: $color = $elmo->favorite_color(); Changes occur in a transaction in the current context: $elmo->favorite_color('blue'); Non-scalar (has_many) properties have a variety of accessors: @jobs = $elmo->jobs(); $jobs = $elmo->job_arrayref(); $set = $elmo->job_set(); $iter = $elmo->job_iterator(); $job = $elmo->add_job($snore); $success = $elmo->remove_job($sing); Query the current context to find objects: $existing_obj = Acme::Puppet->get(name => 'Elmo'); # same reference as $existing_obj @existing_objs = Acme::Puppet->get( favorite_color => ['red','yellow'], ); # this will not get elmo because his favorite color is now blue @existing_objs = Acme::Puppet->get(job => $snore); # this will return $elmo along with other puppets that snore, # though we haven't saved the change yet.. Save our changes: UR::Context->current->commit; Too many puppets...: $elmo->delete; $elmo->play; # this will throw an exception now $elmo = Acme::Puppet->get(name => 'Elmo'); # this returns nothing now Just kidding: UR::Context->current->rollback; # not a database rollback, an in-memory undo All is well: $elmo = Acme::Puppet->get(name => 'Elmo'); # back again! =head1 DESCRIPTION UR::Objects are transactional, queryable, representations of entities, built to maintain separation between the physical reference in a program, and the logical entity the reference represents, using a well-defined interface. UR uses that separation to automatically handle I/O. It provides a query API, and manages the difference between the state of entities in the application, and their state in external persistance systems. It aims to do so transparently, keeping I/O logic orthogonally to "business logic", and hopefully making code around I/O unnecessary to write at all for most programs. Rather than explicitly constructing and serializing/deserializing objects, the application layer just requests objects from the current "context", according to their characteristics. The context manages database connections, object state changes, references, relationships, in-memory transactions, queries and caching in tunable ways. Accessors dynamically fabricate references lazily, as needed through the same query API, so objects work as the developer would traditionally expect in most cases. The goal of UR::Object is that your application doesn't have to do data management. Just ask for what you want, use it, and let it go. UR::Objects support full reflection and meta-programming. Its meta-object layer is fully self-bootstrapping (most classes of which UR is composed are themselves UR::Objects), so the class data can introspect itself, such that even classes can be created within transactions and discarded. =head1 INHERITANCE UR::ModuleBase Basic error, warning, and status messages for modules in UR. UR::Object This class - general OO transactional OO features =head1 WRITING CLASSES See L for a narrative explanation of how to write clases. For a complete reference see L. For the meta-object API see L. A simple example, declaring the class used above: class Acme::Puppet { id_by => 'name', has_optional => [ father => { is => 'Acme::Puppet' }, mother => { is => 'Acme::Puppet' }, jobs => { is => 'Acme::Job', is_many => 1 }, ] }; You can also declare the same API, but specifying additional internal details to make database mapping occur the way you'd like: class Acme::Puppet { id_by => 'name', has_optional => [ father => { is => 'Acme::Puppet', id_by => 'father_id' }, mother => { is => 'Acme::Puppet', id_by => 'mother_id' }, }, has_many_optional => [ job_assignments => { is => 'Acme::PuppetJob', im_its => 'puppet' }, jobs => { is => 'Acme::Job', via => 'job_assignments', to => 'job' }, ] }; =head1 CONSTRUCTING OBJECTS New objects are returned by create() and get(), which delegate to the current context for all object construction. The create() method will always create something new or will return undef if the identity is already known to be in use. The get() method lets the context internally decide whether to return a cached reference for the specified logical entities or to construct new objects by loading data from the outside. =head1 METHODS The examples below use $obj where an actual object reference is required, and SomeClass where the class name can be used. In some cases the example in the synopsisis is continued for deeper illustration. =head2 Base API =over 4 =item get $obj = SomeClass->get($id); $obj = SomeClass->get(property1 => value1, ...); @obj = SomeClass->get(property1 => value1, ...); @obj = SomeClass->get('property1 operator1' => value1, ...); Query the current context for objects. It turns the passed-in parameters into a L and returns all objects of the given class which match. The current context determines whether the request can be fulfilled without external queries. Data is loaded from underlying database(s) lazliy as needed to fulfuill the request. In the simplest case of requesting an object by id which is cached, the call to get() is an immediate hash lookup, and is very fast. See L, or look at L, L, and L for details. If called in scalar context and more than one object matches the given parameters, get() will raise an exception through C. =item create $obj = SomeClass->create( property1 => $value1, properties2 => \@values2, ); Create a new entity in the current context, and return a reference to it. The only required property to create an object is the "id", and that is only required for objects which do not autogenerate their own ids. This requirement may be overridden in subclasses to be more restrictive. If entities of this type persist in an underlying context, the entity will not appear there until commit. (i.e. no insert is done until just before a real database commit) The object in question does not need to pass its own constraints when initially created, but must be fully valid before the transaction which created it commits. =item delete $obj->delete Deletes an object in the current context. The $obj reference will be garbage collected at the discretion of the Perl interpreter as soon as possible. Any attempt to use the reference after delete() is called will result in an exception. If the represented entity was loaded from the parent context (i.e. persistent database objects), it will not be deleted from that context (the database) until commit is called. The commit call will do both the delete and the commit, presuming the complete save works across all involved data sources. Should the transaction roll-back, the deleted object will be re-created in the current context, and a fresh reference will later be returnable by get(). See the documentation on L for details on how deleted objects are rememberd and removed later from the database, and how deleted objects are re-constructed on STM rollback. =item copy $obj->copy(%overrides) Copies the existing C<$obj> by copying the values of all direct properties, except for ID properties, to a newly created object of the same type. A list of params and values may be provided as overrides to the existing values or to specify an ID. =item class $class_name = $obj->class; $class_name = SomeClass->class; Returns the name of the class of the object in question. See __meta__ below for the class meta-object. =item id $id = $obj->id; The unique identifier of the object within its class. For database-tracked entities this is the primary key value, or a composite blob containing the primary key values for multi-column primary keys. For regular objects private to the process, the default id embeds the hostname, process ID, and a timestamp to uniquely identify the UR::Context::Process object which is its final home. When inheritance is involved beneath UR::Object, the 'id' may identify the object within the super-class as well. It is also possible for an object to have a different id upon sub-classification. =back =head2 Accessors Every relationship declared in the class definition results in at least one accesor being generated for the class in question. Identity properties are read-only, while non-identity properties are read-write unless is_mutable is explicitly set to false. Assigning an invalid value is allowed temporarily, but the current transaction will be in an invalid state until corrected, and will not be commitable. The return value of an the accessor when it mutates the object is the value of the property after the mutation has occurred. =head3 Single-value property accessors: By default, properties are expected to return a single value. =over 4 =item NAME Regular accessors have the same name as the property, as declared, and also work as mutators as is commonly expected: $value = $obj->property_name; $obj->property_name($new_value); When the property is declared with id_by instead of recording the refereince, it records the id of the object automatically, such that both will return different values after either changes. =back =head3 Muli-value property accessors: When a property is declared with the "is_many" flag, a variety of accessors are made available on the object. See C for more details on the ways to declare relationships between objects when writing classes. Using the example from the synopsis: =over 4 =item NAMEs (the property name pluralized) A "has_many" relationship is declared using the plural form of the relationship name. An accessor returning the list of property values is generated for the class. It is usable with or without additional filters: @jobs = $elmo->jobs(); @fun_jobs = $elmo->jobs(is_fun => 1); The singular name is used for the remainder of the accessors... =item NAME (the property name in singular form) Returns one item from the group, which must be specified in parameters. If more than one item is matched, an exception is thrown via die(): $job = $elmo->job(name => 'Sing'); $job = $elmo->job(is_fun => 1); # die: too many things are fun for Elmo =item NAME_list The default accessor is available as *_list. Usable with or without additional filters: @jobs = $elmo->job_list(); @fun_jobs = $elmo_>job_list(is_fun => 1); =item NAME_set Return a L value representing the values with *_set: $set = $elmo->job_set(); $set = $elmo->job_set(is_hard => 1); =item NAME_iterator Create a new iterator for the set of property values with *_iterator: $iter = $elmo->job_iterator(); $iter = $elmo->job_iterator(is_fun => 1, -order_by => ['name]); while($obj = $iter->next()) { ... } =item add_NAME Add an item to the set of values with add_*: $added = $elmo->add_job($snore); A variation of the above will construt the item and add it at once. This second form of add_* automatically would identify that the line items also reference the order, and establish the correct converse relationship automatically. @lines = $order->lines; # 2 lines, for instance $line = $order->add_line( product => $p, quantity => $q, ); print $line->num; # 3, if the line item has a multi-column primary key with auto_increment on the 2nd column called num =item remove_NAME Items can be removed from the assigned group in a way symetrical with how they are added: $removed = $elmo->remove_job($sing); =back =head2 Extended API These methods are available on any class defined by UR. They are convenience methods around L, L, L, L, L and L. =over 4 =item create_iterator $iter = SomeClass->create_iterator( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); while (my $obj = $iter->next) { ... } Takes the same sort of parameters as get(), but returns a L for the matching objects. The next() method will return one object from the resulting set each time it is called, and undef when the results have been exhausted. C instances are normal object references in the current process, not context-oriented UR::Objects. They vanish upon dereference, and cannot be retrieved by querying the context. When using an iterator, the system attempts to return objects matching the params at the time the iterator is created, even if those objects do not match the params at the time they are returned from next(). Consider this case: # many objects in the DB match this my $iter = SomeClass->create_iterator(job => 'cleaner'); my $an_obj = SomeClass->get(job => 'cleaner', id => 1); $an_obj->job('messer-upper'); # This no longer matches the iterator's params my @iter_objs; while (my $o = $iter->next) { push @iter_objs, $o; } At the end, @iter_objs will contain several objects, including the object with id 1, even though its job is no longer 'cleaner'. However, if an object matching the iterator's params is deleted between the time the iterator is created and the time next() would return that object, then next() will throw an exception. =item define_set $set = SomeClass->define_set( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); @subsets = $set->group_by('property3','property4'); @some_members = $subsets[0]->members; Takes the same sort of parameters as get(), but returns a set object. Sets are lazy, and only query underlying databases as much as necessary. At any point in time the members() method returns all matches to the specified parameters. See L for details. =item define_boolexpr $bx = SomeClass->define_boolexpr( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); $bx->evaluate($obj1); # true or false? Takes the same sort of parameters as get(), but returns a L object. The boolean expression can be used to evaluate other objects to see if they match the given condition. The "id" of the object embeds the complete "where clause", and as a semi-human-readable blob, such is reconstitutable from it. See L for details on how to use this to do advanced work on defining sets, comparing objects, creating query templates, adding object constraints, etc. =item add_observer $o = $obj1->add_observer( aspect => 'someproperty' callback => sub { print "change!\n" }, ); $obj1->property1('new value'); # observer callback fires.... $o->delete; Adds an observer to an object, monitoring one or more of its properties for changes. The specified callback is fired upon property changes which match the observation request. See L for details. =item create_mock $mock = SomeClass->create_mock( property1 => $value, method1 => $return_value, ); Creates a mock object using using the class meta-data for "SomeClass" via L. Useful for test cases. =back =head2 Meta API The folowing methods allow the application to interrogate UR for information about the object in question. =over 4 =item __meta__ $class_obj = $obj->__meta__(); Returns the class metadata object for the given object's class. Class objects are from the class L, and hold information about the class' properties, data source, relationships to other classes, etc. =item __extend_namespace__ package Foo::Bar; class Foo::Bar { has => ['stuff','things'] }; sub __extend_namespace__ { my $class = shift; my $ext = shift; return class {$class . '::' . $ext} { has => ['more'] }; } Dynamically generate new classes under a given namespace. This is called automatically by UR::ModuleLoader when an unidentified class name is used. If Foo::Bar::Baz is not a UR class, and this occurs: Foo::Bar::Baz->some_method() This is called: Foo::Bar->__extend_namespace__("Baz") If it returns a new class meta, the code will proceed on as though the class had always existed. If Foo::Bar does not exist, the above will be called recursively: Foo->__extend_namespace__("Bar") If Foo::Bar, whether loaded or generated, cannot extend itself for "Baz", the loader will go up the tree before giving up. This means a top-level module could dynamically define classes for any given class name used under it: Foo->__extend_namespace__("Bar::Baz") =item __errors__ @tags = $obj->__errors__() Return a list of L values describing the issues which would prevent a commit in the current transaction. The base implementation check the validity of an object by applying any constraints layed out in the class such as making sure any non-optional properties contain values, numeric properties contain numeric data, and properties with enumerated values only contain valid values. Sub-classes can override this method to add additional validity checking. =item __display_name__ $text = $obj->__display_name__; # the class and id of $obj, by default $text = $line_item->__display_name__($order); Stringifies an object. Some classes may choose to actually overload the stringification operator with this method. Even if they do not, this method will still attempt to identify this object in text form. The default returns the class name and id value of the object within a string. It can be overridden to do a more nuanced job. The class might also choose to overload the stringification operator itself with this method, but even if it doesn not the system will presume this method can be called directly on an object for reasonable stringificaiton. =item __context__ $c = $self->__context__; Return the L for the object reference in question. In UR, a "context" handles connextions between objects, instead of relying on having objects directly reference each other. This allows an object to have a relationship with a large number of other logical entities, without having a "physical" reference present within the process in question. All attempts to resolve non-primitive attribute access go through the context. =back =head2 Extension API These methods are primarily of interest for debugging, for test cases, and internal UR development. They are likely to change before the 1.0 release. =over 4 =item __signal_change__ Called by all mutators to tell the current context about a state change. =item __changes__ @tags = $obj->__changes__() @tags = $obj->__changes__('prop1', 'prop2', ...) Return a list of changes present on the object _directly_. This is really only useful internally because the boundary of the object is internal/subjective. Callers may also request only changes to particular properties. Changes to objects' properties are tracked by the system. If an object has been changed since it was defined or loaded from its external data source, then changed() will return a list of L objects describing which properties have been changed. Work is in-progress on an API to request the portion of the changes in effect in the current transaction which would impact the return value of a given list of properties. This would be directly usable by a view/observer. =item __define__ This is used internally to "virtually load" things. Simply assert they already existed externally, and act as though they were just loaded... It is used for classes defined in the source code (which is the default) by the "class {}" magic instead of in some database, as we'd do for regular objects. =item __strengthen__ $obj->__strengthen__(); Mark this object as unloadable by the object cache pruner. UR objects are normally tracked by the current Context for the life of the application, but the programmer can specify a limit to cache size, in which case old, unchanged objects are periodically pruned from the cache. If strengthen() is called on an object, it will effectively be locked in the cache, and will not be considered for pruning. See L for more information about the pruning mechanism. =item is_strengthened Check if an object has been stengthened, C<__stengthen__>. =item __weaken__ $obj->__weaken__(); Give a hint to the object cache pruner that this instance is not going to be used in the application in the future, and should be removed with preference when pruning the cache. =item is_weakened Check if an object has been weakened, C<__weaken__>. =item DESTROY Perl calls this method on any object before garbage collecting it. It should never by called by your application explicitly. The DESTROY handler is overridden in UR::Object. If you override it in a subclass, be sure to call $self->SUPER::DESTROY() before exiting your override, or errors will occur. =back =head1 ERRORS, WARNINGS and STATUS MESSAGES When an error occurs which is "exceptional" the API will throw an exception via die(). In some cases, when the possibility of failure is "not-exceptional", the method will simply return false. In scalar context this will be undef. In list context an empty list. When there is ambiguity as to whether this is an error or not (get() for instance, might simply match zero items, ...or fail to understand your parameters), an exception is used. =over 4 =item error_message The standard way to convey the error which has occurred is to set ->error_message() on the object. This will propagate to the class, and through its inheritance. This is much like DBI's errstr method, which affects the handle on which it was called, its source handle, and the DBI package itself. =item warning_message Calls to warning_message also record themselves on the object in question, and its class(es). They also emit a standard Perl warn(), which will invoke $SIG{__WARN__}; =item status_message Calls to status_message are also recorded on the object in question. They can be monitored through hooks, as can the other messages. =back See L for more information. =head1 SEE ALSO L, L, L L, L, L, L L contains additional methods which are deprecated in the API. =cut Accessorized.pm100664023532023421 1243612544604516 17141 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Accessorized; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::Object'], ); #--- just because I'm tired of GSCApp and Class::Accessor not playing nice, here we go sub delegate{ my $class = shift; my %p = @_; foreach my $get_object_method(keys %p){ foreach my $delegated_function(@{$p{$get_object_method}}){ my $class_function = $class.'::'.$delegated_function; no strict; *$class_function = sub{ my $self = shift; my $obj = $self->$get_object_method; #--- get the object of delgation unless($obj){ $self->error_message("Failed to call $function on $self"); return; } return $obj->$delegated_function(@_); }; } } 1; } sub ro_delegate{ my $class = shift; my %p = @_; foreach my $function (keys %p){ foreach my $delegator_func(@_){ my $class_function = $class.'::'.$delegator_func; no strict; *$class_function = sub{ my $self = shift; my $obj = $self->$function(); #--- get the object of delgation unless($obj){ $self->error_message("Failed to call $function on $self"); return; } return $obj->$delegator_func(); }; } } 1; } sub accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $setfunc = $class.'::'.$accessor_func; no strict; *$setfunc = sub{ my $self = shift; if(@_){ return $self->__set($accessor_func, @_); } return $self->__get($accessor_func); }; } } sub explicit_accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $write_func = $class.'::set_'.$accessor_func; my $read_func = $class.'::get_'.$accessor_func; no strict; *$write_func = sub{ my $self = shift; return unless @_; return $self->__set($accessor_func, @_); }; *$read_func = sub{ my $self = shift; return unless @_; return $self->__get($accessor_func, @_); }; } } sub ro_accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $setfunc = $class.'::get_'.$accessor_func; no strict; *$setfunc = sub{ my $self = shift; if(@_){ die "cannot set values for read only accessor $accessor_func"; } return $self->__get($accessor_func); }; } } sub ro_array_accessorize{ my $class = shift; foreach my $accessor_func(@_){ no strict; #--- get my $getf = $class.'::'.$accessor_func; *$getf = sub{ my $self = shift; return $self->__get_array($accessor_func); }; } } sub array_accessorize{ my $class = shift; foreach my $accessor_func(@_){ no strict; #--- get my $getf = $class.'::get_'.$accessor_func; *$getf = sub{ my $self = shift; return $self->__get_array($accessor_func); }; #--- set my $setf = $class.'::set_'.$accessor_func; *$setf = sub{ my $self = shift; return $self->__set_array($accessor_func, @_); }; #--- add my $addf = $class.'::add_'.$accessor_func; *$addf = sub{ my $self = shift; return $self->__add_array($accessor_func, @_); }; #--- remove my $removef = $class.'::remove_'.$accessor_func; *$removef = sub{ my $self = shift; return $self->__remove_array($accessor_func, @_); }; #--- clear my $clearf = $class.'::clear_'.$accessor_func; *$clearf = sub{ my $self = shift; return $self->__clear_array($accessor_func); }; #--- default unless($class->can($accessor_func)){ my $defaultf = $class.'::'.$accessor_func; *$defaultf = sub{ my $self = shift; if(@_){ #--- with parameters, it is 'set' return $self->__set_array($accessor_func, @_); } else{ return $self->__get_array($accessor_func, @_); } }; } } } sub __get{ my $self = shift; my $func = shift; return unless ref $self; return $self->{$func}; } sub __set{ my $self = shift; my $func = shift; return unless @_; return unless ref $self; $self->{$func} = shift; return 1; } sub __get_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY'; if(@_){ if(@_ == 1){ return $self->{$func}->[shift]; } else{ return @{$self->{$func}}[@_]; } } return @{$self->{$func}}; } sub __set_array{ my $self = shift; my $func = shift; return unless @_; $self->{$func} = [@_]; 1; } sub __add_array{ my $self = shift; my $func = shift; unless(exists $self->{$func}){ $self->__set_array($func => @_); return 1; } return unless ref $self->{$func} eq 'ARRAY'; push @{$self->{$func}}, @_; 1; } sub __remove_from_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY' && @{$self->{$func}}; my $count = 0; my %bad = map {$_ => 1} @_; my $i=0; while($i < scalar(@{$self->{$func}})){ if($bad{$self->{$func}[$i]}){ splice @{$self->{$func}}, $i, 1; ++$count; next; } ++$i; } return $count; } sub __clear_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY' && @{$self->{$func}}; $self->{$func} = []; return; } 1; FetchAndDo.pm100664023532023421 2144512544604516 20040 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Commandpackage UR::Object::Command::FetchAndDo; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; use Data::Dumper; class UR::Object::Command::FetchAndDo { is => 'Command', is_abstract => 1, has => [ subject_class => { is => 'UR::Object::Type', id_by => 'subject_class_name', }, filter => { is => 'Text', is_optional => 1, doc => 'Filter results based on the parameters. See below for how to.' }, _fields => { is_many => 1, is_optional => 1, doc => 'Methods which the caller intends to use on the fetched objects. May lead to pre-fetching the data.' }, ], }; ######################################################################## sub help_detail { my $class = shift; return $class->_filter_doc; } sub _filter_doc { my $class = shift; my $doc = < and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- EOS if (my $help_synopsis = $class->help_synopsis) { $doc .= " $help_synopsis\n"; } else { $doc .= <200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe EOS } $doc .= <create; if ( not $self->subject_class_name and my $subject_class_name = $self->_resolved_params_from_get_options->{subject_class_name} ) { $self = $class->create(subject_class_name => $subject_class_name); } if ( $self->subject_class_name ) { if ( my @properties = $self->_subject_class_filterable_properties ) { my $longest_name = 0; foreach my $property ( @properties ) { my $name_len = length($property->property_name); $longest_name = $name_len if ($name_len > $longest_name); } for my $property ( @properties ) { my $property_doc = $property->doc; unless ($property_doc) { eval { foreach my $ancestor_class_meta ( $property->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property->property_name); if ($ancestor_property_meta and $ancestor_property_meta->doc) { $property_doc = $ancestor_property_meta->doc; last; } } }; } $property_doc ||= ' (undocumented)'; $property_doc =~ s/\n//gs; # Get rid of embeded newlines my $data_type = $property->data_type || ''; $data_type = ucfirst(lc $data_type); $doc .= sprintf(" %${longest_name}s ($data_type): $property_doc\n", $property->property_name); } } else { $doc .= sprintf(" %s\n", $self->error_message); } } else { $doc .= " Can't determine the list of filterable properties without a subject_class_name"; } return $doc; } ######################################################################## sub execute { my $self = shift; $self->_validate_subject_class or return; my $iterator = $self->_fetch or return; return $self->_do($iterator); } sub _validate_subject_class { my $self = shift; my $subject_class_name = $self->subject_class_name; $self->error_message("No subject_class_name indicated.") and return unless $subject_class_name; $self->error_message( sprintf( 'This command is not designed to work on a base UR class (%s).', $subject_class_name, ) ) and return if $subject_class_name =~ /^UR::/; UR::Object::Type->use_module_with_namespace_constraints($subject_class_name); my $subject_class = $self->subject_class; $self->error_message( sprintf( 'Can\'t get class meta object for class (%s). Is this class a properly declared UR::Object?', $subject_class_name, ) ) and return unless $subject_class; $self->error_message( sprintf( 'Can\'t find method (all_property_metas) in %s. Is this a properly declared UR::Object class?', $subject_class_name, ) ) and return unless $subject_class->can('all_property_metas'); return 1; } sub _subject_class_filterable_properties { my $self = shift; $self->_validate_subject_class or return; my %props = map { $_->property_name => $_ } $self->subject_class->property_metas; return map { $_->[1] } # These maps are to get around a bug in perl 5.8 sort { $a->[0] cmp $b->[0] } # sort involving methdo calls inside the sort sub that map { [ $_->property_name, $_ ] } # might do sorts of their own grep { substr($_->property_name, 0, 1) ne '_' } # Skip 'private' properties starting with '_' grep { ! $_->data_type or index($_->data_type, '::') == -1 } # Can't filter object-type properties from a lister, right? values %props; } sub _hint_string { return; } sub _base_filter { return; } sub _complete_filter { my $self = shift; return join(',', grep { defined $_ } $self->_base_filter,$self->filter); } sub _fetch { my $self = shift; my ($bool_expr, %extra) = UR::BoolExpr->resolve_for_string( $self->subject_class_name, $self->_complete_filter, $self->_hint_string ); $self->error_message( sprintf('Unrecognized field(s): %s', join(', ', keys %extra)) ) and return if %extra; if (my $i = $self->subject_class_name->create_iterator($bool_expr)) { return $i; } else { $self->error_message($self->subject_class_name->error_message); return; } } sub _do { shift->error_message("Abstract class. Please implement a '_do' method in your subclass."); return; } 1; =pod =head1 NAME UR::Object::Command::FetchAndDo - Base class for fetching objects and then performing a function on/with them. =head1 SYNOPSIS package MyFecthAndDo; use strict; use warnings; use above "UR"; class MyFecthAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ # other properties... ], }; sub _do { # required my ($self, $iterator) = @_; while (my $obj = $iterator->next) { ... } return 1; } 1; =head1 Provided by the Developer =head2 _do (required) Implement this method to 'do' unto the iterator. Return true for success, false for failure. sub _do { my ($self, $iterator) = @_; while (my $obj = $iterator->next) { ... } return 1; } =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/distro/ur-bundle/trunk/lib/UR/Object/Command/FetchAndDo.pm $ #$Id: FetchAndDo.pm 47408 2009-06-01 03:53:45Z ssmith $# List.pm100664023532023421 3546612544604516 17024 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Commandpackage UR::Object::Command::List; use strict; use warnings; use IO::File; use Data::Dumper; require Term::ANSIColor; use UR; use UR::Object::Command::List::Style; use List::Util qw(reduce); use Command::V2; use Carp qw(); our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Command::List { is => 'Command::V2', has_input => [ subject_class_name => { is => 'ClassName', doc => 'the type of object to list', }, filter => { is => 'Text', is_optional => 1, doc => 'Filter results based on the parameters. See below for details.', shell_args_position => 1, }, show => { is => 'Text', is_optional => 1, doc => 'Specify which columns to show, in order. Prefix with "+" or "^" to append/prepend to the default list.', }, order_by => { is => 'Text', is_optional => 1, doc => 'Output rows are listed sorted by these named columns in increasing order.', }, ], has_param => [ style => { is => 'Text', is_optional => 1, valid_values => [qw/text csv tsv pretty html xml newtext/], default_value => 'text', doc => 'The output format.', }, csv_delimiter => { is => 'Text', is_optional => 1, default_value => ',', doc => 'For the "csv" output style, specify the field delimiter for something besides a comma.', }, noheaders => { is => 'Boolean', is_optional => 1, default => 0, doc => 'Include headers. Set --noheaders to turn headers off.', }, ], has_transient => [ output => { is => 'IO::Handle', is_optional =>1, is_transient =>1, default => \*STDOUT, doc => 'output handle for list, defauls to STDOUT', }, _fields => { is_many => 1, is_optional => 1, doc => 'Methods which the caller intends to use on the fetched objects. May lead to pre-fetching the data.', }, ], doc => 'lists objects matching the specified expression', }; sub sub_command_sort_position { .2 }; sub create { my $class = shift; my $self = $class->SUPER::create(@_); if (defined($self->csv_delimiter) and ($self->csv_delimiter ne $self->__meta__->property_meta_for_name('csv_delimiter')->default_value) and ($self->style ne 'csv') ) { $self->error_message('--csv-delimiter is only valid when used with --style csv'); return; } unless ( ref $self->output ){ my $ofh = IO::File->new("> ".$self->output); $self->error_message("Can't open file handle to output param ".$self->output) and die unless $ofh; $self->output($ofh); } return $self; } sub _resolve_boolexpr { my $self = shift; my ($bool_expr, %extra) = UR::BoolExpr->resolve_for_string( $self->subject_class_name, $self->_complete_filter, $self->_hint_string, $self->order_by, ); if (%extra) { Carp::croak( sprintf( 'Cannot list for class %s because some items in the filter or show were not properties of that class: %s', $self->subject_class_name, join(', ', keys %extra) ) ); } return $bool_expr; } # Used by create() and execute() to distinguish whether an item from the show list # is likely a property of the subject class or a more complicated expression that needs # to be eval-ed later sub _show_item_is_property_name { my($self, $item) = @_; return $item =~ m/^[\w\.]+$/; } sub execute { my $self = shift; my $subject_class_name = $self->subject_class_name; # ensure classes can be loaded from whatever namespace the subject class has # TODO: make the UR command open the door for the type loading below to hit # all namespaces when _it_ is running only. The ur commands are sw maint tools. my ($ns) = ($subject_class_name =~ /^(.*?)::/); eval "use $ns"; my $subject_class = UR::Object::Type->get($subject_class_name); # Determine things to show my @fields = $self->_resolve_field_list; my $bool_expr = $self->_resolve_boolexpr(); return unless (defined $bool_expr); # TODO: instead of using an iterator, get all the results back in a list and # have the styler use the list, since it needs all the results to space the columns # out properly anyway my $iterator; unless ($iterator = $self->subject_class_name->create_iterator($bool_expr)) { $self->error_message($self->subject_class_name->error_message); return; } my $style_module_name = __PACKAGE__ . '::' . ucfirst $self->style; my $style_module = $style_module_name->new( iterator => $iterator, show => \@fields, csv_delimiter => $self->csv_delimiter, noheaders => $self->noheaders, output => $self->output, ); $style_module->format_and_print; return 1; } sub _resolve_field_list { my $self = shift; if ( my $show = $self->show ) { if (substr($show,0,1) =~ /([\+\^\-])/) { # if it starts with any of the special characters, combine with the default my $default = $self->__meta__->property('show')->default_value; unless ($default) { $default = join(",", map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name)); } $show = join(',',$default,$show); } my @show; my $expr; my @parts = (split(/,/, $show)); my $append_prepend_or_omit = '+'; my $prepend_count = 0; for my $item (@parts) { if ($item =~ /^([\+\^\-])/) { if ($1 eq '^') { $prepend_count = 0; } $append_prepend_or_omit = $1; $item = substr($item,1); } if ($self->_show_item_is_property_name($item) and not defined $expr) { if ($append_prepend_or_omit eq '+') { # append push @show, $item; } elsif ($append_prepend_or_omit eq '^') { # prepend splice(@show, $prepend_count, 0, $item); $prepend_count++; } elsif ($append_prepend_or_omit eq '-') { # omit @show = grep { $_ ne $item } @show; } else { die "unrecognized operator in show string: $append_prepend_or_omit"; } } else { if ($expr) { $expr .= ',' . $item; } else { $expr = '(' . $item; } my $o; if (eval('sub { ' . $expr . ')}')) { push @show, $expr . ')'; #print "got: $expr<\n"; $expr = undef; } } } if ($expr) { die "Bad expression: $expr\n$@\n"; } return @show; } else { return map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name); } } sub _filter_doc { my $class = shift; my $doc = <18" # > is a special character name='Bob Jones' # spaces in a field value Standard and/or predicated logic is supported (like in SQL). "name='Bob Jones' and job='Captain' and age>18" "name='Betty Jones' and (score < 10 or score > 100)" The "like" operator uses "%" as a wildcard: "name like '%Jones'" The "not" operator negates the condition: "name not like '%Jones'" Use square brackets for "in" clauses. "name like '%Jones' and job in [Captain,Ensign,'First Officer']" Use a dot (".") to indirectly access related data (joins): "age<18 and father.address.city='St. Louis'" "previous_order.items.price > 100" A shorthand filter form allows many queries to be written more concisely: regular: "name = 'Jones' and age between 18-25 and happy in ['yes','no','maybe']" shorthand: name~%Jones,age:18-25,happy:yes/no/maybe Shorthand Key: -------------- , " and " = exactly equal to ~ "like" the value : "between" two values, dash "-" separated : "in" the list of several values, slash "/" separated ! "not" operator can be combined with any of the above EOS if (my $help_synopsis = $class->help_synopsis) { $doc .= "\n Examples:\n ---------\n"; $doc .= " $help_synopsis\n"; } # Try to get the subject class name my $self = $class->create; if ( not $self->subject_class_name and my $subject_class_name = $self->_resolved_params_from_get_options->{subject_class_name} ) { $self = $class->create(subject_class_name => $subject_class_name); } my @properties = $self->_properties_for_class_to_document($self->subject_class_name); my @filterable_properties = grep { ! $_->data_type or index($_->data_type, '::') == -1 } @properties; my @relational_properties = grep { $_->data_type and index($_->data_type, '::') >= 0 } @properties; my $longest_name = 0; foreach my $property ( @properties ) { my $name_len = length($property->property_name); $longest_name = $name_len if ($name_len > $longest_name); } my @data; if ( ! $self->subject_class_name ) { $doc .= " Can't determine the list of properties without a subject_class_name.\n"; } elsif ( ! @properties ) { $doc .= sprintf(" %s\n", $self->error_message); } else { if (@filterable_properties) { push @data, 'Simple Properties:'; for my $property ( @filterable_properties ) { push @data, [$property->property_name, $self->_doc_for_property($property, $longest_name)]; } } if (@relational_properties) { push @data, 'Complex Properties (support dot-syntax):'; for my $property ( @relational_properties ) { my $name = $property->property_name; my @doc = $self->_doc_for_property($property,$longest_name); push @data, [$name, $doc[0]]; for my $n (1..$#doc) { push @data, ['', $doc[$n]]; } } } } my @lines = $class->_format_property_doc_data(@data); { no warnings 'uninitialized'; $doc .= join("\n ", @lines); } $self->delete; return $doc; } sub _doc_for_property { my $self = shift; my $property = shift; my $longest_name = shift; my $doc; my $property_doc = $property->doc; unless ($property_doc) { eval { foreach my $ancestor_class_meta ( $property->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property->property_name); if ($ancestor_property_meta and $ancestor_property_meta->doc) { $property_doc = $ancestor_property_meta->doc; last; } } }; } $property_doc ||= ''; $property_doc =~ s/\n//gs; # Get rid of embeded newlines my $data_type = $property->data_type; my $data_class = eval { $property->_data_type_as_class_name }; if ($data_type and $data_class eq $data_type) { my @has = $self->_properties_for_class_to_document($data_class); my @labels; for my $pmeta (@has) { my $name = $pmeta->property_name; my $type = $pmeta->data_type; if ($type and $type =~ /::/) { push @labels, "$name\[.*\]"; } else { push @labels, $name; } } return ( ($property_doc ? $property_doc : ()), " see for more details", ' has: ' . join(", ", @labels), '', ); } else { $data_type ||= 'Text'; $data_type = (index($data_type, '::') == -1) ? ucfirst(lc $data_type) : $data_type; if ($property_doc) { $property_doc = '(' . $data_type . '): ' . $property_doc; } else { $property_doc = '(' . $data_type . ')'; } return $property_doc; } } sub _format_property_doc_data { my ($class, @data) = @_; my @names = map { $_->[0] } grep { ref $_ } @data; my $longest_name = reduce { length($a) > length($b) ? $a : $b } @names; my $w = length($longest_name); my @lines; for my $data (@data) { if (ref $data) { push @lines, sprintf(" %${w}s %s", $data->[0], $data->[1]); } else { push @lines, ' ', $data, '-' x length($data); } } return @lines; } sub _properties_for_class_to_document { my $self = shift; my $target_class_name = shift; my $target_class_meta = $target_class_name->__meta__; my @id_by = $target_class_meta->id_properties; my @props = $target_class_meta->properties; no warnings; # These final maps are to get around a bug in perl 5.8 sort # involving method calls inside the sort sub that may # do sorts of their own return map { $_->[1] } sort { $a->[1]->position_in_module_header <=> $b->[1]->position_in_module_header or $a->[0] cmp $b->[0] } map { [ $_->property_name, $_ ] } grep { substr($_->property_name, 0, 1) ne '_' and not $_->implied_by and not $_->is_transient and not $_->is_deprecated } @props; } sub _base_filter { return; } sub _complete_filter { my $self = shift; return join(',', grep { defined $_ } $self->_base_filter,$self->filter); } sub help_detail { my $self = shift; return join( "\n", $self->_style_doc, $self->_filter_doc, ); } sub _style_doc { return <_show_item_is_property_name($_) } $self->_resolve_field_list(); return join(',',@show_parts); } 1; List.pod100664023532023421 331512544604516 17136 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Command=pod =head1 NAME UR::Object::Command::List - Fetches and lists objects in different styles. =head1 SYNOPSIS package MyLister; use strict; use warnings; use above "UR"; class MyLister { is => 'UR::Object::Command::List', has => [ # add/modify properties ], }; 1; =head1 Provided by the Developer =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 show (optional) Add defaults to the show property: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ show => { default_value => 'name,age', }, ], }; =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =head1 List Styles text, csv, html, xml, pretty (inprogress) =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/perl_modules/trunk/UR/Object/Command/List.pm $ #$Id: List.pm 50329 2009-08-25 20:10:00Z abrummet $ Style.pm100664023532023421 2517112544604516 20114 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Command/Listpackage UR::Object::Command::List::Style; our $VERSION = "0.44"; # UR $VERSION; sub new { my ($class, %args) = @_; foreach (qw/iterator show noheaders output/){ die "no value for $_!" unless defined $args{$_}; } return bless(\%args, $class); } sub _get_next_object_from_iterator { my $self = shift; my $obj; for (1) { $obj = eval { $self->{'iterator'}->next }; if ($@) { UR::Object::Command::List->warning_message($@); redo; } } return $obj; } sub _object_properties_to_string { my ($self, $o, $char) = @_; my @v; return join( $char, map { defined $_ ? $_ : '' } map { $self->_object_property_to_string($o,$_) } @{$self->{show}} ); } sub _object_property_to_string { my ($self, $o, $property) = @_; my @v; if (substr($property,0,1) eq '(') { @v = eval $property; if ($@) { @v = (''); # ($@ =~ /^(.*)$/); } } else { @v = (); foreach my $i ($o->__get_attr__($property)) { if (! defined $i) { push @v, ""; } elsif (Scalar::Util::blessed($i) and $i->isa('UR::Value') and $i->can('create_view')) { # Here we allow any UR::Values that have their own views to present themselves. my $v = $i->create_view( perspective => 'default', toolkit => 'text' ); push @v, $v->content(); } elsif (Scalar::Util::blessed($i) and $i->can('__display_name__')) { push @v, $i->__display_name__; } else { push @v, $i; } } } if (@v > 1) { no warnings; return join(' ',@v); } else { return $v[0]; } } sub format_and_print{ my $self = shift; unless ( $self->{noheaders} ) { $self->{output}->print($self->_get_header_string. "\n"); } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $self->{output}->print($self->_get_object_string($object), "\n"); $count++; } } package UR::Object::Command::List::Html; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; return "". join("", map { uc } @{$self->{show}}) .""; } sub _get_object_string{ my ($self, $object) = @_; my $out = ""; for my $property ( @{$self->{show}} ){ $out .= "" . $object->$property . ""; } return $out . ""; } sub format_and_print{ my $self = shift; $self->{output}->print(""); #cannot use super because \n screws up javascript unless ( $self->{noheaders} ) { $self->{output}->print($self->_get_header_string); } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $self->{output}->print($self->_get_object_string($object)); $count++; } $self->{output}->print("
    "); } package UR::Object::Command::List::Csv; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; my $delimiter = $self->{'csv_delimiter'}; return join($delimiter, map { lc } @{$self->{show}}); } sub _get_object_string { my ($self, $object) = @_; return $self->_object_properties_to_string($object, $self->{'csv_delimiter'}); } package UR::Object::Command::List::Tsv; use base 'UR::Object::Command::List::Csv'; sub _get_header_string{ my $self = shift; my $delimiter = "\t"; return join($delimiter, map { lc } @{$self->{show}}); } sub _get_object_string { my ($self, $object) = @_; return $self->_object_properties_to_string($object, "\t"); } package UR::Object::Command::List::Pretty; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ return ''; } sub _get_object_string{ my ($self, $object) = @_; my $out; for my $property ( @{$self->{show}} ) { my $value = join(', ', $self->_object_property_to_string($object,$property)); $out .= sprintf( "%s: %s\n", Term::ANSIColor::colored($property, 'red'), Term::ANSIColor::colored($value, 'cyan'), ); } return $out; } package UR::Object::Command::List::Xml; use base 'UR::Object::Command::List::Style'; sub format_and_print{ my $self = shift; my $out; eval "use XML::LibXML"; if ($@) { die "Please install XML::LibXML (run sudo cpanm XML::LibXML) to use this tool!"; } my $doc = XML::LibXML->createDocument(); my $results_node = $doc->createElement("results"); $results_node->addChild( $doc->createAttribute("generated-at",$UR::Context::current->now()) ); $doc->setDocumentElement($results_node); my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { my $object_node = $results_node->addChild( $doc->createElement("object") ); my $object_reftype = ref $object; $object_node->addChild( $doc->createAttribute("type",$object_reftype) ); $object_node->addChild( $doc->createAttribute("id",$object->id) ); for my $property ( @{$self->{show}} ) { my $property_node = $object_node->addChild ($doc->createElement($property)); my @items = $object->$property; my $reftype = ref $items[0]; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { foreach (@items) { my $subobject_node = $property_node->addChild( $doc->createElement("object") ); $subobject_node->addChild( $doc->createAttribute("type",$reftype) ); $subobject_node->addChild( $doc->createAttribute("id",$_->id) ); #$subobject_node->addChild( $doc->createTextNode($_->id) ); #xIF } } else { foreach (@items) { $property_node->addChild( $doc->createTextNode($_) ); } } } $count++; } $self->{output}->print($doc->toString(1)); } package UR::Object::Command::List::Text; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; return join ( "\n", join("\t", map { uc } @{$self->{show}}), join("\t", map { '-' x length } @{$self->{show}}), ); } sub _get_object_string{ my ($self, $object) = @_; $self->_object_properties_to_string($object, "\t"); } sub format_and_print{ my $self = shift; my $tab_delimited; unless ($self->{noheaders}){ $tab_delimited .= $self->_get_header_string."\n"; } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $tab_delimited .= $self->_get_object_string($object)."\n"; $count++; } $self->{output}->print($self->tab2col($tab_delimited)); } sub tab2col{ my ($self, $data) = @_; #turn string into 2d array of arrayrefs ($array[$rownum][$colnum]) my @rows = split("\n", $data); @rows = map { [split("\t", $_)] } @rows; my $output; my @width; #generate array of max widths per column foreach my $row_ref (@rows) { my @cols = @$row_ref; my $index = $#cols; for (my $i = 0; $i <= $index; $i++) { my $l = (length $cols[$i]) + 3; #TODO test if we need this buffer space $width[$i] = $l if ! defined $width[$i] or $l > $width[$i]; } } #create a array of blanks to use as a templatel my @column_template = map { ' ' x $_ } @width; #iterate through rows and cols, substituting in the row entry in your template foreach my $row_ref (@rows) { my @cols = @$row_ref; my $index = $#cols; #only apply template for all but the last entry in a row for (my $i = 0; $i < $index; $i++) { my $entry = $cols[$i]; my $template = $column_template[$i]; substr($template, 0, length $entry, $entry); $output.=$template; } $output.=$cols[$index]."\n"; #Don't need traling spaces on the last entry } return $output; } package UR::Object::Command::List::Newtext; use base 'UR::Object::Command::List::Text'; sub format_and_print{ my $self = shift; my $tab_delimited; unless ($self->{noheaders}){ $tab_delimited .= $self->_get_header_string."\n"; } my $view = UR::Object::View->create( subject_class_name => 'UR::Object', perspective => 'lister', toolkit => 'text', aspects => [ @{$self->{'show'}} ], ); my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $view->subject($object); $tab_delimited .= $view->content() . "\n"; $count++; } $self->{output}->print($self->tab2col($tab_delimited)); } 1; =pod =head1 NAME UR::Object::Command::List - Fetches and lists objects in different styles. =head1 SYNOPSIS package MyLister; use strict; use warnings; use above "UR"; class MyLister { is => 'UR::Object::Command::List', has => [ # add/modify properties ], }; 1; =head1 Provided by the Developer =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 show (optional) Add defaults to the show property: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ show => { default_value => 'name,age', }, ], }; =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =head1 List Styles text, csv, html, xml, pretty (inprogress) =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/perl_modules/trunk/UR/Object/Command/List.pm $ #$Id: List.pm 50329 2009-08-25 20:10:00Z abrummet $ Ghost.pm100664023532023421 772212544604516 15571 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object##### # # Support "Ghost" objects. These represent deleted items which are not saved. # They are omitted from regular class lists. # ##### package UR::Object::Ghost; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; sub _init_subclass { my $class_name = pop; no strict; no warnings; my $live_class_name = $class_name; $live_class_name =~ s/::Ghost$//; *{$class_name ."\:\:class"} = sub { "$class_name" }; *{$class_name ."\:\:live_class"} = sub { "$live_class_name" }; } sub create { Carp::croak('Cannot create() ghosts.') }; sub delete { Carp::croak('Cannot delete() ghosts.') }; sub __rollback__ { my $self = shift; # revive ghost object my $ghost_copy = eval("no strict; no warnings; " . Data::Dumper::Dumper($self)); if ($@) { Carp::confess("Error re-constituting ghost object: $@"); } my($saved_data, $saved_key); if (exists $ghost_copy->{'db_saved_uncommitted'} ) { $saved_data = $ghost_copy->{'db_saved_uncommitted'}; } elsif (exists $ghost_copy->{'db_committed'} ) { $saved_data = $ghost_copy->{'db_committed'}; } else { return; # This shouldn't happen?! } my $new_object = $self->live_class->UR::Object::create(%$saved_data); $new_object->{db_committed} = $ghost_copy->{db_committed} if (exists $ghost_copy->{'db_committed'}); $new_object->{db_saved_uncommitted} = $ghost_copy->{db_saved_uncommitted} if (exists $ghost_copy->{'db_saved_uncommitted'}); unless ($new_object) { Carp::confess("Failed to re-constitute $self!"); } return $new_object; } sub _load { shift->is_loaded(@_); } sub unload { return; } sub __errors__ { return; # Ghosts are always valid, don't check their properties } sub edit_class { undef } sub ghost_class { undef } sub is_ghost { return 1; } sub live_class { my $class = $_[0]->class; $class =~ s/::Ghost//; return $class; } my @ghost_changes; sub changed { @ghost_changes = UR::Object::Tag->create ( type => 'changed', properties => ['id']) unless @ghost_changes; return @ghost_changes; } sub AUTOSUB { # Delegate to the similar function on the regular class. my ($func, $self) = @_; my $live_class = $self->live_class; return $live_class->can($func); } 1; =pod =head1 NAME UR::Object::Ghost - Abstract class for representing deleted objects not yet committed =head1 SYNOPSIS my $obj = Some::Class->get(1234); $obj->some_method(); $obj->delete(); # $obj is now a UR::DeletedRef $ghost = Some::Class::Ghost->get(1234); $ghost->some_method; # Works =head1 DESCRIPTION Ghost objects are a bookkeeping entity for tracking objects which have been loaded from an external data source, deleted within the application, and not yet committed. This implies that they still exist in the external data source. When the Context is committed, the existence of Ghost objects triggers commands to the external data sources to also delete the object(s). When objects are brought into the Context by querying a data source, they are compared against any ghosts that may already exist, and matching objects are not re-loaded or returned to the user from a call to get(). If the user wants to get Ghost objects, they must call get() explicitly on the Ghost class. Each class in the system also has an associated Ghost class, the name of which is formed by tacking '::Ghost' to the name of the regular class. Ghost classes do not have ghosts themselves. Instances of Ghosts are not instantiated with create() directly, they are created as a concequence of deleting a regular object instance. A Ghost can be turned back into a "live" object by re-creating it, or rolling back the transaction it was deleted in. =head1 DEPRECATED Applications will not, and should not, normally interact with Ghosts. The whole Ghost system is scheduled for elimination as we refactor the Context and software transaction framework. =head1 SEE ALSO UR::Object, UR::Object::Type =cut Index.pm100664023532023421 5106112544604516 15567 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object# Index for cached objects. package UR::Object::Index; our $VERSION = "0.44"; # UR $VERSION;; use base qw(UR::Object); use strict; use warnings; require UR; use List::MoreUtils; # wrapper for one of the ID properties to make it less ugly sub indexed_property_names { my $self = shift; unless (exists $self->{indexed_property_names}) { no warnings; $self->{indexed_property_names} = [ split(/,/,$self->{indexed_property_string}) ]; } return @{$self->{indexed_property_names}}; } sub indexed_property_numericness { my $self = shift; unless (exists $self->{indexed_property_numericness}) { my $class_meta = $self->indexed_class_name->__meta__; my @is_numeric = map { my @props = $class_meta->_concrete_property_meta_for_class_and_name($_); @props == 1 ? $props[0]->is_numeric : 0 # multiple ID properties are treated as a string } $self->indexed_property_names; $self->{indexed_property_numericness} = \@is_numeric; } return @{ $self->{indexed_property_numericness} }; } # the only non-id property has an accessor... sub data_tree { if (@_ > 1) { my $old = $_[0]->{data_tree}; my $new = $_[1]; if ($old ne $new) { $_[0]->{data_tree} = $new; $_[0]->__signal_change__('data_tree', $old, $new); } return $new; } return $_[0]->{data_tree}; } # override create to initilize the index sub create { my $class = shift; # NOTE: This is called from one location in UR::Context and relies # on all properties including the ID being specifically defined. my $self = $UR::Context::current->_construct_object($class, @_); return unless $self; $self->{data_tree} ||= {}; $self->_build_data_tree; $self->_setup_change_subscription; $self->__signal_change__("create"); return $self; } # this does a lookup as efficiently as possible sub get_objects_matching { my $self = shift; my @values = @_; # The hash access below generates warnings # where undef is a value. Ignore these. no warnings 'uninitialized'; my @hr = ($self->{data_tree}); my @is_numeric = $self->indexed_property_numericness; my $iter = List::MoreUtils::each_array(@values, @is_numeric); while(my($value, $is_numeric) = $iter->()) { my $value_ref = ref($value); if($value_ref eq "HASH") { # property => { operator => "not like", value => "H~_WGS%", escape "~" } if (my $op = $value->{operator}) { $op = lc($op); my $not = 0; if ($op =~ m/^(!|not\s*)(.*)/) { $not = 1; $op = $2; } my $result; if ($op eq '=' and !$not) { @hr = grep { $_ } map { $_->{$value->{'value'}} } @hr; } elsif ($op eq 'like') { my $comparison_value = $value->{value}; my $escape = $value->{escape}; my $regex = UR::BoolExpr::Template::PropertyComparison::Like-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); my @thr; if ($not) { # Get the values using the regular or negative match op. foreach my $h (@hr) { foreach my $k (sort keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here if($k !~ /$regex/) { push @thr, $h->{$k}; } } } } else { # Standard positive match for my $h (@hr) { for my $k (sort keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here if ($k =~ /$regex/) { push @thr, $h->{$k}; } } } } @hr = grep { $_ } @thr; } elsif ($op eq 'in' and !$not) { $value = $value->{value}; my $has_null = ( (grep { length($_) == 0 } @$value) ? 1 : 0); if ($has_null) { @hr = grep { $_ } map { @$_{@$value} } @hr; } else { my @value = grep { length($_) > 0 } @$value; @hr = grep { $_ } map { @$_{@value} } @hr; } } elsif ($op eq 'in' and $not) { $value = $value->{value}; # make a hash if we got an array as a value #die ">@$value<" if ref($value) eq "ARRAY"; $value = { map { $_ => 1 } @$value } if ref($value) eq "ARRAY"; # if there is a single null, the not in clause will be false if ($value->{""}) { @hr = (); } else { # return everything NOT in the hash my @thr; for my $h (@hr) { for my $k (sort keys %$h) { next unless length($k); unless ($value->{$k}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } } elsif ($op eq 'isa') { my @thr; foreach my $h ( @hr ) { foreach my $k ( keys %$h) { if ($k->isa($value->{value}) xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif ($op eq 'true' or $op eq 'false') { $not = (( $op eq 'true' && $not) or ($op eq 'false' && !$not)); my @thr; foreach my $h ( @hr ) { foreach my $k ( keys %$h ) { if ($k xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif ($not and ($op eq '=' or !$op)) { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { # An empty string for $k means the object's value was loaded as NULL # and we want things like 0 != NULL to be true to match the SQL that # gets generated for the same rule my $t = ($k eq '') || ($is_numeric ? $k != $value->{value} : $k ne $value->{value}); if ($t) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '>') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here my $t = $is_numeric ? $k > $value->{value} : $k gt $value->{value}; if ($t xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here my $t = $is_numeric ? $k < $value->{value} : $k lt $value->{value}; if ($t xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '>=') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here my $t = $is_numeric ? $k >= $value->{value} : $k ge $value->{value}; if ($t xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<=') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here my $t = $is_numeric ? $k <= $value->{value} : $k le $value->{value}; if ($t xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq 'ne') { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { next if $k eq ''; # an earlier undef value got saved as an empty string here if($k ne $value->{value} xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<>') { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { if((length($k) and length($value->{value}) and $k ne $value->{value}) xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq 'between') { my @thr; my ($min,$max) = @{ $value->{value} }; foreach my $h (@hr) { foreach my $k (sort keys %$h) { next if $k eq ''; my $t = $is_numeric ? ( $k >= $min and $k <= $max ) : ( $k ge $min and $k le $max ); if ($t xor $not) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } else { use Data::Dumper; Carp::confess("Unknown operator in key-value pair used in index lookup for index " . Dumper($value)); } } else { Carp::confess("No operator specified in hashref value!" . Dumper($value)); } } elsif (not $value_ref) { # property => value @hr = grep { $_ } map { $_->{$value} } @hr; } elsif ($value_ref eq "ARRAY") { # property => [ v1, v2, v3] @hr = grep { $_ } map { @$_{@$value} } @hr; } } return (map { values(%$_) } @hr); } # private methods sub _build_data_tree { my $self = $_[0]; my @indexed_property_names = $self->indexed_property_names; my $hr_base = $self->{data_tree}; # _remove_object in bulk. %$hr_base = (); my $indexed_class_name = $self->indexed_class_name; if (my @bad_properties = grep { not $indexed_class_name->can($_) } @indexed_property_names ) { Carp::confess( "Attempt to index $indexed_class_name by properties which " . "do not function: @bad_properties" ); } # _add_object in bulk. for my $object ($UR::Context::current->all_objects_loaded($indexed_class_name)) { my(@values, $hr); if (@indexed_property_names) { @values = map { my $val = $object->$_; defined $val ? $val : undef } @indexed_property_names; @values = (undef) unless(@values); } $hr = $hr_base; for my $value (@values) { no warnings 'uninitialized'; # in case $value is undef $hr->{$value} ||= {}; $hr = $hr->{$value}; } my $obj_id = $object->id; $hr->{$obj_id} = $object; if (Scalar::Util::isweak($UR::Context::all_objects_loaded->{$indexed_class_name}->{$obj_id})) { Scalar::Util::weaken($hr->{$obj_id}); } } } # FIXME maybe objects in an index should always be weakend? sub weaken_reference_for_object { my $self = shift; my $object = shift; my $overrides = shift; # FIXME copied from _remove_object - what's this for? no warnings; my @indexed_property_names = $self->indexed_property_names; my @values = map { ($overrides && exists($overrides->{$_})) ? $overrides->{$_} : $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr = $hr->{$value}; return unless $hr; } Scalar::Util::weaken($hr->{$object->id}); } sub _setup_change_subscription { my $self = shift; my $indexed_class_name = $self->indexed_class_name; my @indexed_property_names = $self->indexed_property_names; if (1) { # This is a new indexing strategy which pays at index creation time instead of use. my @properties_to_watch = (@indexed_property_names, qw/create delete load unload/); #print "making index $self->{id}\n"; for my $class ($indexed_class_name, @{ $UR::Object::Type::_init_subclasses_loaded{$indexed_class_name} }) { for my $property (@properties_to_watch) { my $index_list = $UR::Object::Index::all_by_class_name_and_property_name{$class}{$property} ||= []; #print " adding to $class\n"; push @$index_list, $self; } } return 1; } # This will be ignored for now. # If the __signal_change__/subscription system is improved, it may be better to go back? my %properties_to_watch = map { $_ => 1 } (@indexed_property_names, qw/create delete load unload/); $self->{_get_change_subscription} = $indexed_class_name->create_subscription( callback => sub { my ($changed_object, $changed_property, $old_value, $new_value) = @_; #print "got change $changed_property for $indexed_class_name: $changed_object->{id}: @_\n"; # ensure we don't track changes for subclasses #return() unless ref($changed_object) eq $indexed_class_name; # ensure we only add/remove for selected method calls return() unless $properties_to_watch{$_[1]}; #print "changing @_\n"; $self->_remove_object( $changed_object, { $changed_property => $old_value } ) if ($changed_property ne 'create' and $changed_property ne 'load' and $changed_property ne '__define__'); $self->_add_object($changed_object) if ($changed_property ne 'delete' and $changed_property ne 'unload'); }, note => "index monitor " . $self->id, priority => 0, ); } sub _get_change_subscription { # accessor for the change subscription $_[0]->{_get_change_subscription} = $_[1] if (@_ > 1); return $_[0]->{_get_change_subscription}; } sub _remove_object($$) { no warnings; my ($self, $object, $overrides) = @_; my @indexed_property_names = $self->indexed_property_names; my @values = map { ($overrides && exists($overrides->{$_})) ? $overrides->{$_} : $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr = $hr->{$value}; } delete $hr->{$object->id}; } sub _add_object($$) { # We get warnings when undef converts into an empty string. # For efficiency, we turn warnings off in this method. no warnings; my ($self, $object) = @_; my @indexed_property_names = $self->indexed_property_names; my @values = map { $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr->{$value} ||= {}; $hr = $hr->{$value}; } $hr->{$object->id} = $object; # This is the exact formula used elsewhere. TODO: refactor, base on class meta if ($UR::Context::light_cache and substr($self->indexed_class_name,0,5) ne 'App::') { Scalar::Util::weaken($hr->{$object->id}); } } sub _all_objects_indexed { my $self = shift; my @object_hashes = ( $self->{data_tree} ); # Recurse one level deep for each indexed property name # and collect the hashes at that level foreach ( $self->indexed_property_names ) { my @new_object_hashes; while (my $hr = shift @object_hashes) { push @new_object_hashes, values(%$hr); } @object_hashes = @new_object_hashes; } # The final level's values are all the objects return map { values %$_ } @object_hashes; } 1; =pod =head1 NAME UR::Object::Index - Indexing system for retrieving objects by non-id properties =head1 DESCRIPTION This class implements an indexing system for objects to retrieve them quickly by properties other than their ID properties. Their existence and use is managed by the Context as needed, and end-users should never need to interact with UR::Object::Index instances. Internally, they are a container for objects of the same class and a set of properties used to look them up. Each time a get() is performed on a new set of non-id properties, a new Index is created to handle the request for objects which may already exist in the object cache, The data_tree inside the Index is a multi-level hash. The levels are in the same order as the properties in the get request. At each level, the hash keys are the values that target property has. For that level and key, all the objects inside have the same value for that property. A get() by three non-id properties will have a 3-level hash. =cut Iterator.pm100664023532023421 603512544604516 16272 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Iterator; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; our @CARP_NOT = qw( UR::Object ); # These are no longer UR Objects. They're regular blessed references that # get garbage collected in the regular ways #use UR; # #UR::Object::Type->define( # class_name => __PACKAGE__, # has => [ # filter_rule_id => {}, # ], #); # #sub create_for_filter_rule { # my $class = shift; # my $filter_rule = shift; # my $code = $UR::Context::current->get_objects_for_class_and_rule($filter_rule->subject_class_name,$filter_rule,undef,1); # # my $self = $class->SUPER::create( # # TODO: some bug with frozen items? # filter_rule_id => $filter_rule->id, # ); # # $self->_iteration_closure($code); # return $self; #} sub create { die "Don't call UR::Object::Iterator->create(), use create_for_filter_rule() instead"; } sub create_for_filter_rule { my $class = shift; my $filter_rule = shift; my $code = $UR::Context::current->get_objects_for_class_and_rule($filter_rule->subject_class_name,$filter_rule,undef,1); my $self = bless { filter_rule_id => $filter_rule->id, _iteration_closure => $code}, __PACKAGE__; return $self; } sub _iteration_closure { my $self = shift; if (@_) { return $self->{_iteration_closure} = shift; } $self->{_iteration_closure}; } sub next { shift->{_iteration_closure}->(@_); } 1; =pod =head1 NAME UR::Object::Iterator - API for iterating through objects matching a rule =head1 SYNOPSIS my $rule = UR::BoolExpr->resolve('Some::Class', foo => 1); my $iter = UR::Object::Iterator->create_for_filter_rule($rule); while (my $obj = $iter->next()) { print "Got an object: ",$obj->id,"\n"; } # Equivalent my $iter2 = Some::Class->create_iterator(foo => 1); while (my $obj = $iter2->next()) { print "Got an object: ",$obj->id,"\n"; } =head1 DESCRIPTION get(), implemented in UR::Object, is the usual way for retrieving sets of objects matching particular properties. When the result set of data is large, it is often more efficient to use an iterator to access the data instead of getting it all in one list. UR::Object implements create_iterator(), which is just a wrapper around create_for_filter_rule(). UR::Object::Iterator instances are normal Perl object references, not UR-based objects. They do not live in the Context's object cache, and obey the normal Perl rules about scoping. =head1 METHODS =over 4 =item create_for_filter_rule $iter = UR::Object::Iterator->create_for_filter_rule($boolexpr); Creates an iterator object based on the given BoolExpr (rule). Under the hood, it calls get_objects_for_class_and_rule() on the current Context with the $return_closure flag set to true. =item next $obj = $iter->next(); Return the next object matching the iterator's rule. When there are no more matching objects, it returns undef. =back =head1 SEE ALSO UR::Object, UR::Context =cut Join.pm100664023532023421 4153412544604516 15423 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Join; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; our @CARP_NOT = qw( UR::Object::Property ); class UR::Object::Join { #is => 'UR::Value', id_by => [ id => { is => 'Text' }, ], has_optional_transient => [ source_class => { is => 'Text' }, source_property_names => { is => 'Text' }, foreign_class => { is => 'Text' }, foreign_property_names => { is => 'Text' }, source_name_for_foreign => { is => 'Text' }, foreign_name_for_source => { is => 'Text' }, is_optional => { is => 'Boolean' }, is_many => { is => 'Boolean' }, sub_group_label => { is => 'Text' }, where => { is => 'Text' }, ], doc => "join metadata used internally by the ::QueryBuilder" }; our %resolve_chain; # When a Join is unloaded, we need to remove from the cache any join chain # using this join sub unload { my $self = shift; my $id_to_remove = $self->id; foreach my $joins_for_class ( values %resolve_chain ) { foreach my $property_chain ( keys %$joins_for_class ) { # need to skip over DeletedRefs that may already be in the list if (grep { $_->isa('UR::Object::Join') and ($_->id eq $id_to_remove) } @{$joins_for_class->{$property_chain}} ) { # This unloaded join is in the list - nuke the whole list delete $joins_for_class->{$property_chain}; } } } $self->SUPER::unload(@_); } sub resolve_chain { my ($class, $class_name, $property_chain) = @_; my $join_chain = $resolve_chain{$class_name}{$property_chain} ||= do { my $class_meta = $class_name->__meta__; my @pmeta = $class_meta->property_meta_for_name($property_chain); my @joins; for my $pmeta (@pmeta) { push @joins, $class->_resolve_chain_for_property_meta($pmeta); } \@joins; }; return @$join_chain; } sub _resolve_chain_for_property_meta { my ($class, $pmeta) = @_; if ($pmeta->via or $pmeta->to) { return $class->_resolve_via_to($pmeta); } else { my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { return; } if ($pmeta->id_by or $foreign_class->isa("UR::Value")) { return $class->_resolve_forward($pmeta); } elsif (my $reverse_as = $pmeta->reverse_as) { return $class->_resolve_reverse($pmeta); } else { # TODO: handle hard-references to objects here maybe? $pmeta->error_message("Property '" . $pmeta->property_name . "' of class " . $pmeta->class_name . " has no 'id_by' or 'reverse_as' property metadata"); return; } } } sub _get_or_define { my $class = shift; my %p = @_; my $id = delete $p{id}; delete $p{__get_serial}; delete $p{db_committed}; delete $p{_change_count}; delete $p{__defined}; my $self = $class->get(id => $id); unless ($self) { $self = $class->__define__($id); for my $k (keys %p) { $self->$k($p{$k}); no warnings; unless ($self->{$k} eq $p{$k}) { Carp::confess(Data::Dumper::Dumper($self, \%p)); } } } unless ($self) { Carp::confess("Failed to create join???"); } return $self; } sub _resolve_via_to { my ($class, $pmeta) = @_; my $class_name = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $class_name); my @joins; my $via = $pmeta->via; my $to = $pmeta->to; if ($via and not $to) { $to = $pmeta->property_name; } my $via_meta; if ($via) { if ($via eq '__self__') { my $to_meta = $class_meta->property_meta_for_name($to); unless ($to_meta) { my $property_name = $pmeta->property_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata 'to' property '$to'"; } return $to_meta->_resolve_join_chain(); } $via_meta = $class_meta->property_meta_for_name($via); unless ($via_meta) { return if $class_name->can($via); # It's via a method, not an actual property my $property_name = $pmeta->property_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata for via property '$via'"; } if ($via_meta->to and ($via_meta->to eq '-filter')) { return $via_meta->_resolve_join_chain(); } unless ($via_meta->data_type) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No data type for via property '$via'"; } push @joins, $via_meta->_resolve_join_chain(); if (my $where = $pmeta->where) { my $join = pop @joins; unless ($join and $join->{foreign_class}) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak("Can't resolve joins for property '$property_name' of $class_name: Couldn't determine foreign class for via property '$via'\n" . "join data so far: ". Data::Dumper::Dumper($join, \@joins)); } my $where_rule = UR::BoolExpr->resolve($join->{foreign_class}, @$where); my $id = $join->{id}; $id .= ' ' . $where_rule->id; my %join_data = %$join; push @joins, $class->_get_or_define(%join_data, id => $id, where => $where, sub_group_label => $pmeta->property_name); } } else { $via_meta = $pmeta; } if ($to and $to ne '__self__' and $to ne '-filter') { my $to_class_meta = eval { $via_meta->data_type->__meta__ }; unless ($to_class_meta) { Carp::croak("Can't get class metadata for " . $via_meta->data_type . " while resolving property '" . $pmeta->property_name . "' in class " . $pmeta->class_name . "\n" . "Is the data_type for property '" . $via_meta->property_name . "' in class " . $via_meta->class_name . " correct?"); } my $to_meta = $to_class_meta->property_meta_for_name($to); unless ($to_meta) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak "Can't resolve property '$property_name' of $class_name: No '$to' property found on " . $via_meta->data_type; } push @joins, $to_meta->_resolve_join_chain(); } if (my $return_class_name = $pmeta->_convert_data_type_for_source_class_to_final_class($pmeta->data_type, $pmeta->class_name)) { my $final_class_name = $joins[-1]->foreign_class; if ($return_class_name ne $final_class_name) { if ($return_class_name->isa($final_class_name)) { # the property is a subclass of the one involved in the final join # this happens when there is a via/where/to where say "to" goes-to any "Animal" but this overall property is known to be a "Dog". my $general_join = pop @joins; my $specific_join = UR::Object::Join->_get_or_define( source_class => $general_join->{'source_class'}, source_property_names => $general_join->{'source_property_names'}, foreign_class => $return_class_name, # more specific foreign_property_names => $general_join->{'foreign_property_names'}, # presume the borrow took you into a subclass and these still work is_optional => $general_join->{'is_optional'}, id => $general_join->{id} . ' isa ' . $return_class_name ); push @joins, $specific_join; } elsif ($return_class_name eq 'UR::Value::SloppyPrimitive' or $final_class_name eq 'UR::Value::SloppyPrimitive') { # backward-compatible layer for before there were primitive types } elsif ($final_class_name->isa($return_class_name)) { Carp::carp("Joins for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name . " is declared as data type $return_class_name while its joins connect to a more specific data type $final_class_name!"); } else { #Carp::carp("Discrepant join for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name # . ". Its data type ($return_class_name) does not match the join from property '" # . join("','", @{$joins[-1]->{source_property_names}}) . "' of class " . $joins[-1]->{source_class} # . " with type $final_class_name"); } } } return @joins; } # code below uses these to convert objects using hash slices my @old = qw/source_class source_property_names foreign_class foreign_property_names source_name_for_foreign foreign_name_for_source is_optional is_many sub_group_label/; my @new = qw/foreign_class foreign_property_names source_class source_property_names foreign_name_for_source source_name_for_foreign is_optional is_many sub_group_label/; sub _resolve_forward { my ($class, $pmeta) = @_; my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { #Carp::cluck("No metadata?!"); return; } my $source_class = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name); my @joins; my $where = $pmeta->where; my $foreign_class_meta = $foreign_class->__meta__; my $property_name = $pmeta->property_name; my $id = $source_class . '::' . $property_name; if ($where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } ##### # direct reference (or primitive, which is a direct ref to a value obj) my (@source_property_names, @source_property_types, @foreign_property_names, @foreign_property_types, $source_name_for_foreign, $foreign_name_for_source); if ($foreign_class->isa("UR::Value")) { if (my $id_by = $pmeta->id_by) { my @id_by = ref($id_by) eq 'ARRAY' ? @$id_by : ($id_by); foreach my $id_by_name ( @id_by ) { my $id_by_property = $class_meta->property_meta_for_name($id_by_name); push @joins, $id_by_property->_resolve_join_chain(); } } @source_property_names = ($property_name); @foreign_property_names = ('id'); $source_name_for_foreign = ($property_name); } elsif (my $id_by = $pmeta->id_by) { my @pairs = $pmeta->get_property_name_pairs_for_join; @source_property_names = map { $_->[0] } @pairs; @foreign_property_names = map { $_->[1] } @pairs; if (ref($id_by) eq 'ARRAY') { # satisfying the id_by requires joins of its own # sms: why is this only done on multi-value fks? foreach my $id_by_property_name ( @$id_by ) { my $id_by_property = $class_meta->property_meta_for_name($id_by_property_name); next unless ($id_by_property and $id_by_property->is_delegated); push @joins, $id_by_property->_resolve_join_chain(); $source_class = $joins[-1]->{'foreign_class'}; @source_property_names = @{$joins[-1]->{'foreign_property_names'}}; } } $source_name_for_foreign = $pmeta->property_name; my @reverse = $foreign_class_meta->properties(reverse_as => $source_name_for_foreign, data_type => $pmeta->class_name); my $reverse; if (@reverse > 1) { my @reduced = grep { not $_->where } @reverse; if (@reduced != 1) { Carp::confess("Ambiguous results finding reversal for $property_name!" . Data::Dumper::Dumper(\@reverse)); } $reverse = $reduced[0]; } else { $reverse = $reverse[0]; } if ($reverse) { $foreign_name_for_source = $reverse->property_name; } } # the foreign class might NOT have a reverse_as, but # this records what to reverse in this case. $foreign_name_for_source ||= '<' . $source_class . '::' . $source_name_for_foreign; push @joins, $class->_get_or_define( id => $id, source_class => $source_class, source_property_names => \@source_property_names, foreign_class => $foreign_class, foreign_property_names => \@foreign_property_names, source_name_for_foreign => $source_name_for_foreign, foreign_name_for_source => $foreign_name_for_source, is_optional => ($pmeta->is_optional or $pmeta->is_many), is_many => $pmeta->is_many, where => $where, ); return @joins; } sub _resolve_reverse { my ($class, $pmeta) = @_; my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { #Carp::cluck("No metadata?!"); return; } my $source_class = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name); my @joins; my $where = $pmeta->where; my $property_name = $pmeta->property_name; my $id = $source_class . '::' . $property_name; if ($where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } ##### my $reverse_as = $pmeta->reverse_as; my $foreign_class_meta = $foreign_class->__meta__; my $foreign_property_via = $foreign_class_meta->property_meta_for_name($reverse_as); unless ($foreign_property_via) { Carp::confess("No property '$reverse_as' in class $foreign_class, needed to resolve property '" . $pmeta->property_name . "' of class " . $pmeta->class_name); } my @join_data = map { { %$_ } } $foreign_property_via->_resolve_join_chain(); my $prev_where = $where; for (@join_data) { @$_{@new} = @$_{@old}; my $next_where = $_->{where}; $_->{where} = $prev_where; no warnings qw(uninitialized); #source_name_for_foreign can be undefined at the end of the chain my $id = $_->{source_class} . '::' . $_->{source_name_for_foreign}; use warnings qw(uninitialized); if ($prev_where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } $_->{id} = $id; $_->{is_optional} = ($pmeta->is_optional || $pmeta->is_many); $_->{is_many} = $pmeta->{is_many}; $_->{sub_group_label} = $pmeta->property_name; $prev_where = $next_where; } @join_data = reverse @join_data; if ($prev_where) { # Having a where clause in the last join is only a problem if testing # the where condition needs more joins. But if it did, then those additional # joins would have already been in the list, right? #Carp::confess("final join needs placement! " . Data::Dumper::Dumper($prev_where)); } for my $join_data (@join_data) { push @joins, $class->_get_or_define(%$join_data); } return @joins; } # Return true if the foreign-end of the join includes all the ID properties of # the foreign class. Used by the ObjectFabricator when it is determining whether or # not to include more rules in the all_params_loaded hash for delegations sub destination_is_all_id_properties { my $self = shift; my $foreign_class_meta = $self->{'foreign_class'}->__meta__; my %join_properties = map { $_ => 1 } @{$self->{'foreign_property_names'}}; my $join_has_all_id_props = 1; foreach my $foreign_id_meta ( $foreign_class_meta->all_id_property_metas ) { next if $foreign_id_meta->class_name eq 'UR::Object'; # Skip the manufactured 'id' property next if (delete $join_properties{ $foreign_id_meta->property_name }); $join_has_all_id_props = 0; } return $join_has_all_id_props; } 1; Property.pm100664023532023421 4760612544604516 16356 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Property; use warnings; use strict; require UR; use Lingua::EN::Inflect; use Class::AutoloadCAN; our $VERSION = "0.44"; # UR $VERSION;; our @CARP_NOT = qw( UR::DataSource::RDBMS UR::Object::Type ); # class_meta and r_class_meta duplicate the functionality if two properties of the same name, # but these are faster sub class_meta { return shift->{'class_name'}->class->__meta__; } sub r_class_meta { return shift->{'data_type'}->class->__meta__; } sub is_direct { my $self = shift; if ($self->is_calculated or $self->is_constant or $self->is_many or $self->via) { return 0; } return 1; } sub is_numeric { my $self = shift; unless (defined($self->{'_is_numeric'})) { my $class = $self->_data_type_as_class_name; unless ($class) { return; } $self->{'_is_numeric'} = $class->isa("UR::Value::Number"); } return $self->{'_is_numeric'}; } sub is_text { my $self = shift; unless (defined($self->{'_is_text'})) { my $class = $self->_data_type_as_class_name; unless ($class) { return; } $self->{'_is_text'} = $class->isa("UR::Value::Text"); } return $self->{'_is_text'}; } sub is_valid_storage_for_value { my($self, $value) = @_; my $data_class_name = $self->_data_type_as_class_name; return 1 if ($value->isa($data_class_name)); if ($data_class_name->isa('UR::Value') ) { my @underlying_types = $data_class_name->underlying_data_types; foreach my $underlying_type ( @underlying_types ) { return 1 if ($value->isa($underlying_type)); } } return 0; } sub alias_for { my $self = shift; if ($self->{'via'} and $self->{'to'} and $self->{'via'} eq '__self__') { return $self->{'to'}; } else { return $self->{'property_name'}; } } sub _convert_data_type_for_source_class_to_final_class { my ($class, $foreign_class, $source_class) = @_; $foreign_class ||= ''; # TODO: allowing "is => 'Text'" instead of is => 'UR::Value::Text' is syntactic sugar # We should have an is_primitive flag set on these so we do efficient work. my ($ns) = ($source_class =~ /^([^:]+)::/); if ($ns and not $ns->isa("UR::Namespace")) { $ns = undef; } my $final_class; if ($foreign_class) { if ($foreign_class->can('__meta__')) { $final_class = $foreign_class; } else { my ($ns_value_class, $ur_value_class); if ($ns and $ns->can("get")) { $ns_value_class = $ns . '::Value::' . $foreign_class; if ($ns_value_class->can('__meta__')) { $final_class = $ns_value_class; } } if (!$final_class) { $ur_value_class = 'UR::Value::' . $foreign_class; if ($ur_value_class->can('__meta__')) { $final_class = $ur_value_class; } } if (!$final_class) { $ur_value_class = 'UR::Value::' . ucfirst(lc($foreign_class)); if ($ur_value_class->can('__meta__')) { $final_class = $ur_value_class; } } } } if (!$final_class) { if (Class::Autouse->class_exists($foreign_class)) { return $foreign_class; } elsif ($foreign_class =~ /::/) { return $foreign_class; } else { eval "use $foreign_class;"; if (!$@) { return $foreign_class; } if (!$ns or $ns->get()->allow_sloppy_primitives) { # no colons, and no namespace: no choice but to assume it's a sloppy primitive return 'UR::Value::SloppyPrimitive'; } else { Carp::confess("Failed to find a ${ns}::Value::* or UR::Value::* module for primitive type $foreign_class!"); } } } return $final_class; } sub _data_type_as_class_name { my $self = $_[0]; return $self->{_data_type_as_class_name} ||= do { my $source_class = $self->class_name; #this is so NUMBER -> Number my $foreign_class = $self->data_type; if (not $foreign_class) { if ($self->via or $self->to) { my @joins = UR::Object::Join->resolve_chain( $self->class_name, $self->property_name, $self->property_name, ); $foreign_class = $joins[-1]->foreign_class; } } __PACKAGE__->_convert_data_type_for_source_class_to_final_class($foreign_class, $source_class); }; } # TODO: this is a method on the data source which takes a given property. # Returns the table and column for this property. # If this particular property doesn't have a column_name, and it # overrides a property defined on a parent class, then walk up the # inheritance and find the right one sub table_and_column_name_for_property { my $self = shift; # Shortcut - this property has a column_name, so the class should have the right # table_name if ($self->column_name) { return ($self->class_name->__meta__->table_name, $self->column_name); } my $property_name = $self->property_name; my @class_metas = $self->class_meta->parent_class_metas; my %seen; while (@class_metas) { my $class_meta = shift @class_metas; next if ($seen{$class_meta}++); my $p = $class_meta->property_meta_for_name($property_name); next unless $p; if ($p->column_name && $class_meta->table_name) { return ($class_meta->table_name, $p->column_name); } push @class_metas, $class_meta->parent_class_metas; } # This property has no column anywhere in the class' inheritance return; } # Return true if resolution of this property involves an ID property of # any class. sub _involves_id_property { my $self = shift; my $is_id = $self->is_id; return 1 if defined($is_id); if ($self->id_by) { my $class_meta = $self->class_meta; my $id_by_list = $self->id_by; foreach my $id_by ( @$id_by_list ) { my $id_by_meta = $class_meta->property_meta_for_name($id_by); return 1 if ($id_by_meta and $id_by_meta->_involves_id_property); } } if ($self->via) { my $via_meta = $self->via_property_meta; return 1 if ($via_meta and $via_meta ne $self and $via_meta->_involves_id_property); if ($self->to) { my $to_meta = $self->to_property_meta; return 1 if ($to_meta and $to_meta->_involves_id_property); if ($self->where) { unless ($to_meta) { Carp::confess("Property '" . $self->property_name . "' of class " . $self->class_name . " has 'to' metadata that does not resolve to a known property."); } my $other_class_meta = $to_meta->class_meta; my $where = $self->where; for (my $i = 0; $i < @$where; $i += 2) { my $where_meta = $other_class_meta->property_meta_for_name($where->[$i]); return 1 if ($where_meta and $where_meta->_involves_id_property); } } } } return 0; } # For via/to delegated properties, return the property meta in the same # class this property delegates through sub via_property_meta { my $self = shift; return unless ($self->is_delegated and $self->via); my $class_meta = $self->class_meta; return $class_meta->property_meta_for_name($self->via); } sub final_property_meta { my $self = shift; my $closure; $closure = sub { return unless defined $_[0]; if ($_[0]->is_delegated and $_[0]->via) { if ($_[0]->to) { return $closure->($_[0]->to_property_meta); } else { return $closure->($_[0]->via_property_meta); } } else { return $_[0]; } }; my $final = $closure->($self); return if !defined $final || $final->id eq $self->id; return $final; } # For via/to delegated properties, return the property meta on the foreign # class that this property delegates to sub to_property_meta { my $self = shift; return unless ($self->is_delegated && $self->to); my $via_meta = $self->via_property_meta(); return unless $via_meta; my $remote_class = $via_meta->data_type; # unless ($remote_class) { # # Can we guess what the data type is for multiply indirect properties? # if ($via_meta->to) { # my $to_property_meta = $via_meta->to_property_meta; # $remote_class = $to_property_meta->data_type if ($to_property_meta); # } # } return unless $remote_class; my $remote_class_meta = UR::Object::Type->get($remote_class); return unless $remote_class_meta; return $remote_class_meta->property_meta_for_name($self->to); } sub get_property_name_pairs_for_join { my ($self) = @_; unless ($self->{'_get_property_name_pairs_for_join'}) { my @linkage = $self->_get_direct_join_linkage(); unless (@linkage) { Carp::croak("Cannot resolve underlying property joins for property '" . $self->property_name . "' of class " . $self->class_name . ": Couldn't determine which properties link to the remote class"); } my @results; if ($self->reverse_as) { @results = map { [ $_->[1] => $_->[0] ] } @linkage; } else { @results = map { [ $_->[0] => $_->[1] ] } @linkage; } $self->{'_get_property_name_pairs_for_join'} = \@results; } return @{$self->{'_get_property_name_pairs_for_join'}}; } sub _get_direct_join_linkage { my ($self) = @_; my @retval; if (my $id_by = $self->id_by) { my $r_class_meta = $self->r_class_meta; unless ($r_class_meta) { Carp::croak("Property '" . $self->property_name . "' of class '" . $self->class_name . "' " . "has data_type '" . $self->data_type ."' with no class metadata"); } my @my_id_by = @{ $self->id_by }; my @their_id_by = @{ $r_class_meta->{'id_by'} }; if (! @their_id_by or (@my_id_by == 1 and @their_id_by > 1) ) { @their_id_by = ( 'id' ); } unless (@my_id_by == @their_id_by) { Carp::croak("Property '" . $self->property_name . "' of class '" . $self->class_name . "' " . "has " . scalar(@my_id_by) . " id_by elements, while its data_type (" . $self->data_type .") has " . scalar(@their_id_by)); } for (my $i = 0; $i < @my_id_by; $i++) { push @retval, [ $my_id_by[$i], $their_id_by[$i] ]; } } elsif (my $reverse_as = $self->reverse_as) { my $r_class_name = $self->data_type; @retval = $r_class_name->__meta__->property_meta_for_name($reverse_as)->_get_direct_join_linkage(); } return @retval; } sub _resolve_join_chain { my $self = shift; return UR::Object::Join->resolve_chain( $self->class_name, $self->property_name, ); } sub label_text { # The name of the property in friendly terms. my ($self,$obj) = @_; my $property_name = $self->property_name; my @words = App::Vocabulary->filter_vocabulary(map { ucfirst(lc($_)) } split(/\s+/,$property_name)); my $label = join(" ", @words); return $label; } # This gets around the need to make a custom property subclass # when a class has an attributes_have specification. # This primary example of this in base infrastructure is that # all Commands have is_input, is_output and is_param attributes. # Note: it's too permissive and will make an accessor for any hash key. # The updated code should not do this. sub CAN { my ($thisclass, $method, $self) = @_; if (ref($self)) { my $accessor_key = '_' . $method . "_accessor"; if (my $method = $self->{$accessor_key}) { return $method; } if ($self->class_name->__meta__->{attributes_have} and exists $self->class_name->__meta__->{attributes_have}{$method} ) { return $self->{$accessor_key} = sub { return $_[0]->{$method}; } } } return; } 1; =pod =head1 NAME UR::Object::Property - Class representing metadata about a class property =head1 SYNOPSIS my $prop = UR::Object::Property->get(class_name => 'Some::Class', property_name => 'foo'); my $class_meta = Some::Class->__meta__; my $prop2 = $class_meta->property_meta_for_name('foo'); # Print out the meta-property name and its value of $prop2 print map { " $_ : ".$prop2->$_ } qw(class_name property_name data_type default_value); =head1 DESCRIPTION Instances of this class represent properties of classes. For every item mentioned in the 'has' or 'id_by' section of a class definition become Property objects. =head1 INHERITANCE UR::Object::Property is a subclass of L =head1 PROPERTY TYPES For this class definition: class Some::Class { has => [ other_id => { is => 'Text' }, other => { is => 'Some::Other', id_by => 'foo_id' }, bar => { via => 'other', to => 'bar' }, foos => { is => 'Some::Foo', reverse_as => 'some', is_many => 1 }, uc_other_id => { calculate_from => 'other_id', calculate_perl => 'uc($other_id)' }, ], }; Properties generally fall in to one of these categories: =over 4 =item regular property A regular property of a class holds a single scalar. In this case, 'other_id' is a regular property. =item object accessor An object accessor property returns objects of some class. The properties of this class must link in some way with all the ID properties of the remote class (the 'is' declaration). 'other' is an object accessor property. This is how one-to-one relationships are implemented. =item via property When a class has some object accessor property, and it is helpful for an object to assumme the value of the remote class's properties, you can set up a 'via' property. In the example above, an object of this class gets the value of its 'bar' property via the 'other' object it's linked to, from that object's 'bar' property. =item reverse as or is many property This is how one-to-many relationships are implemented. In this case, the Some::Foo class must have an object accessor property called 'some', and the 'foos' property will return a list of all the Some::Foo objects where their 'some' property would have returned that object. =item calculated property A calculated property doesn't store its data directly in the object, but when its accessor is called, the calculation code is executed. =back =head1 PROPERTIES Each property has a method of the same name =head2 Direct Properties =over 4 =item class_name => Text The name of the class this Property is attached to =item property_name => Text The name of the property. The pair of class_name and property name are the ID properties of UR::Object::Property =item column_name => Text If the class is backed by a database table, then the column this property's data comes from is stored here =item data_type => Text The type of data stored in this property. Corresponds to the 'is' part of a class's property definition. =item data_length => Number The maximum size of data stored in this property =item default_value For is_optional properties, the default value given when an object is created and this property is not assigned a value. =item valid_values => ARRAY A listref of enumerated values this property may be set to =item doc => Text A place for documentation about this property =item is_id => Boolean Indicates whether this is an ID property of the class =item is_optional => Boolean Indicates whether this is property may have the value undef when the object is created =item is_transient => Boolean Indicates whether this is property is transient? =item is_constant => Boolean Indicates whether this property can be changed after the object is created. =item is_mutable => Boolean Indicates this property can be changed via its accessor. Properties cannot be both constant and mutable =item is_volatile => Boolean Indicates this property can be changed by a mechanism other than its normal accessor method. Signals are not emmitted even when it does change via its normal accessor method. =item is_classwide => Boolean Indicates this property's storage is shared among all instances of the class. When the value is changed for one instance, that change is effective for all instances. =item is_delegated => Boolean Indicates that the value for this property is not stored in the object directly, but is delegated to another object or class. =item is_calculated => Boolean Indicates that the value for this property is not a part of the object'd data directly, but is calculated in some way. =item is_transactional => Boolean Indicates the changes to the value of this property is tracked by a Context's transaction and can be rolled back if necessary. =item is_abstract => Boolean Indicates this property exists in a base class, but must be overridden in a derived class. =item is_concrete => Boolean Antonym for is_abstract. Properties cannot be both is_abstract and is_concrete, =item is_final => Boolean Indicates this property cannot be overridden in a derived class. =item is_deprecated => Boolean Indicates this property's use is deprecated. It has no effect in the use of the property in any way, but is useful in documentation. =item implied_by => Text If this property is created as a result of another property's existence, implied_by is the name of that other property. This can happen in the case where an object accessor property is defined has => [ foo => { is => 'Some::Other', id_by => 'foo_id' }, ], Here, the 'foo' property requires another property called 'foo_id', which is not explicitly declared. In this case, the Property named foo_id will have its implied_by set to 'foo'. =item id_by => ARRAY In the case of an object accessor property, this is the list of properties in this class that link to the ID properties in the remote class. =item reverse_as => Text Defines the linking property name in the remote class in the case of an is_many relationship =item via => Text For a via-type property, indicates which object accessor to go through. =item to => Text For a via-type property, indicates the property name in the remote class to get its value from. The default value is the same as property_name =item where => ARRAY Supplies additional filters for indirect properies. For example: foos => { is => 'Some::Foo', reverse_as => 'some', is_many => 1 }, blue_foos => { via => 'foos', where => [ color => 'blue' ] }, Would create a property 'blue_foos' which returns only the related Some::Foo objects that have 'blue' color. =item calculate_from => ARRAY For calculated properties, this is a list of other property names the calculation is based on =item calculate_perl => Text For calculated properties, a string containing Perl code. Any properties mentioned in calculate_from will exist in the code's scope at run time as scalars of the same name. =item class_meta => UR::Object::Type Returns the class metaobject of the class this property belongs to =back =head1 METHODS =over 4 =item via_property_meta For via/to delegated properties, return the property meta in the same class this property delegates through =item to_property_meta For via/to delegated properties, return the property meta on the foreign class that this property delegates to =back =head1 SEE ALSO UR::Object::Type, UR::Object::Type::Initializer, UR::Object =cut Text.pm100664023532023421 116512544604516 21546 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Property/View/Defaultpackage UR::Object::Property::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['class_name', 'property_name','data_type', 'is_optional'], }, ], ); 1; =pod =head1 NAME UR::Object::Property::View::Default::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Info when displaying information about a property =cut Text.pm100664023532023421 424112544604516 24072 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Property/View/DescriptionLineItempackage UR::Object::Property::View::DescriptionLineItem::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', doc => "View used by 'ur show properties' for each property line item", ); sub _update_view_from_subject { my $self = shift; my $property_meta = $self->subject; return unless ($property_meta); my $nullable = $property_meta->is_optional ? "NULLABLE" : ""; my $column_name = $property_meta->column_name; unless ($column_name) { if ($property_meta->via) { $column_name = $property_meta->via . '->' . $property_meta->to; } elsif ($property_meta->is_classwide) { $column_name = '(classwide)'; } elsif ($property_meta->is_delegated) { # delegated, but not via. Must be an object accessor $column_name = '' } elsif ($property_meta->is_calculated) { my $calc_from = $property_meta->calculate_from; if ($calc_from and @$calc_from) { $column_name = '(calculated from ' . join(',',@$calc_from). ')'; } else { $column_name = '(calculated)'; } } else { $column_name = '(no column)'; } } my $data_type_string; if (defined $property_meta->data_type) { my $len = $property_meta->data_length; $data_type_string = $property_meta->data_type . ( $len ? "(".$len.")" : ""); } else { $data_type_string = '(no type)'; } my $text = sprintf(" %2s %30s %-40s %25s $nullable", $property_meta->is_id ? "ID" : " ", $property_meta->property_name, $column_name, $data_type_string, ); my $widget = $self->widget(); my $buffer_ref = $widget->[0]; $$buffer_ref = $text; return 1; } 1; =pod =head1 NAME UR::Object::Property::View::DescriptionLineItem::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Show::Properties when displaying information about a property =cut Text.pm100664023532023421 332412544604516 24263 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Property/View/ReferenceDescriptionpackage UR::Object::Property::View::ReferenceDescription::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', doc => "View used by 'ur show properties' for each object-accessor property", ); sub _update_view_from_subject { my $self = shift; my $property_meta = $self->subject; return unless ($property_meta); my $r_class_name = $property_meta->data_type; my @relation_detail; my @pairs = eval { $property_meta->get_property_name_pairs_for_join() }; my $text; if (@pairs) { foreach my $pair ( @pairs ) { my($property_name, $r_property_name) = @$pair; push @relation_detail, "$r_property_name => \$self->$property_name"; } my $padding = length($r_class_name) + 34; my $relation_detail = join(",\n" . " "x$padding, @relation_detail); $text = sprintf(" %22s => %s->get(%s)\n", $property_meta->property_name, $r_class_name, $relation_detail); } else { $text = sprintf(" %22s => %s->get(id => \$self->%s)\n", $property_meta->property_name, $r_class_name, $property_meta->property_name); } my $widget = $self->widget(); my $buffer_ref = $widget->[0]; $$buffer_ref = $text; return 1; } 1; =pod =head1 NAME UR::Object::Property::View::DescriptionLineItem::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Show::Properties when displaying information about a property =cut Set.pm100664023532023421 3233712544604516 15260 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Set; use strict; use warnings; use UR; use List::MoreUtils qw(any); our $VERSION = "0.44"; # UR $VERSION; our @CARP_NOT = qw( UR::Object::Type ); use overload ('""' => '__display_name__'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('!=' => sub { $_[0] . '' ne $_[1] . '' } ); use overload ('ne' => sub { $_[0] . '' ne $_[1] . '' } ); class UR::Object::Set { is => 'UR::Value', is_abstract => 1, has => [ rule => { is => 'UR::BoolExpr', id_by => 'id' }, rule_display => { is => 'Text', via => 'rule', to => '__display_name__'}, member_class_name => { is => 'Text', via => 'rule', to => 'subject_class_name' }, members => { is => 'UR::Object', is_many => 1, is_calculated => 1 } ], doc => 'an unordered group of distinct UR::Objects' }; # override the UR/system display name # this is used in stringification overload sub __display_name__ { my $self = shift; my %b = $self->rule->_params_list; my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Dump; $s =~ s/\n/ /gs; $s =~ s/^\s*{//; $s =~ s/\}\s*$//; $s =~ s/\"(\w+)\" \=\> / $1 => /g; return '(' . ref($self) . ' ' . $s . ')'; } # When a set comes into existance, set up a subscription to monitor changes # to the set's members UR::Object::Set->create_subscription( method => 'load', note => 'set creation monitor', callback => sub { my $set = shift; my $rule = $set->rule; my %set_defining_attributes = map { $_ => 1 } $rule->template->_property_names(); my $deps = $set->{__aggregate_deps} ||= {}; $set->member_class_name->create_subscription( note => 'set monitor '.$set->id, priority => 0, callback => sub { return unless exists($set->{__aggregates}); # nothing cached yet my ($member, $attr_name, $before, $after) = @_; # load/unload won't affect aggregate values return if ($attr_name eq 'load' or $attr_name eq 'unload'); # If a set-defining attribute changes, or an object matching # the set is created or deleted, then the set membership has # possibly changed. Invalidate the whole aggregate cache. if (exists($set_defining_attributes{$attr_name}) || ( ($attr_name eq 'create' or $attr_name eq 'delete') && $rule->evaluate($member) ) ) { $set->__invalidate_cache__; # A later call to _members_have_changes() would miss the case # where a member becomes deleted or a member-defining attribute # changes $set->{__members_have_changes} = 1; } # if the changed attribute is a dependancy for a cached aggregation # value, and it's a set member... elsif ((my $dependant_aggregates = $deps->{$attr_name}) && $rule->evaluate($member) ) { # remove the cached aggregates that depend on this attribute delete @{$set->{__aggregates}}{@$dependant_aggregates}; # remove the dependancy records delete @$deps{@$dependant_aggregates}; delete $deps->{$attr_name} } } ); } ); # When a transaction rolls back, it doesn't trigger subscriptions for the # member objects as they get changed back to their original values. # The safe thing is to set wipe out all Sets' aggregate caches :( # It would be helpful if sets had a db_committed like other objects # and we could just revert their values back to their db_committed values UR::Context::Transaction->create_subscription( method => 'rollback', note => 'rollback set cache invalidator', callback => sub { delete(@$_{'__aggregates','__aggregate_deps','__members_have_changes'}) foreach UR::Object::Set->is_loaded(); } ); UR::Context->create_subscription( method => 'commit', callback => sub { my $worked = shift; return unless $worked; # skip if the commit failed delete $_->{__members_have_changes} foreach UR::Object::Set->is_loaded(); } ); sub get_with_special_parameters { Carp::cluck("Getting sets by directly properties of their members method will be removed shortly because of ambiguity on the meaning of 'id'. Please update the code which calls this."); my $class = shift; my $bx = shift; my @params = @_; my $member_class = $class; $member_class =~ s/::Set$//; return $member_class->define_set($bx->params_list, @params); } sub members { my $self = shift; my $rule = $self->rule; while (@_) { $rule = $rule->add_filter(shift, shift); } return $self->member_class_name->get($rule); } sub member_iterator { my $self = shift; my $rule = $self->rule; while (@_) { $rule = $rule->add_filter(shift, shift); } return $self->member_class_name->create_iterator($rule); } sub _members_have_changes { my $self = shift; return 1 if $self->{__members_have_changes}; my @property_names = @_; my $rule = $self->rule; return any { $rule->evaluate($_) && $_->__changes__(@property_names) } $self->member_class_name->is_loaded; } sub subset { my $self = shift; my $member_class_name = $self->member_class_name; my $bx = UR::BoolExpr->resolve($member_class_name,@_); my $subset = $self->class->get($bx->id); return $subset; } sub group_by { my $self = shift; my @group_by = @_; my $grouping_rule = $self->rule->add_filter(-group_by => \@group_by); my @groups = UR::Context->current->get_objects_for_class_and_rule( $self->member_class_name, $grouping_rule, undef, #$load, 0, #$return_closure, ); return $self->context_return(@groups); } sub __invalidate_cache__ { my $self = shift; if (@_) { my $aggregate = shift; delete $self->{__aggregates}->{$aggregate}; } else { delete @$self{'__aggregates','__aggregate_deps'}; } } sub __aggregate__ { my $self = shift; my $aggr = shift; my $f = $aggr->{f}; my $aggr_properties = $aggr->{properties}; Carp::croak("$f is a group operation, and is not writable") if @_; my $subject_class_meta = $self->rule->subject_class_name->__meta__; my $not_ds_expressable = grep { $_->is_calculated or $_->is_transient or $_->is_constant } map { $_->final_property_meta or $_ } map { $subject_class_meta->property_meta_for_name($_) || () } $self->rule->template->_property_names; my($cache, $deps) = @$self{'__aggregates','__aggregate_deps'}; # If there are no member-class objects with changes, we can just interrogate the DB if (! exists($cache->{$f})) { if ($not_ds_expressable or $self->_members_have_changes(@$aggr_properties)) { my $fname; my @fargs; if ($f =~ /^(\w+)\((.*)\)$/) { $fname = $1; @fargs = ($2 ? split(',',$2) : ()); } else { $fname = $f; @fargs = (); } my $local_method = '__aggregate_' . $fname . '__'; $self->{__aggregates}->{$f} = $self->$local_method(@fargs); } else { my $rule = $self->rule->add_filter(-aggregate => [$f])->add_filter(-group_by => []); UR::Context->current->get_objects_for_class_and_rule( $self->member_class_name, $rule, 1, # load 0, # return_closure ); } # keep 2-way mapping of dependances... # First, keep a list of properties this aggregate cached value depends on $deps->{$f} = $aggr_properties; # And add this aggregate to the lists these properties are dependancies for foreach ( @$aggr_properties ) { $deps->{$_} ||= []; push @{$deps->{$_}}, $f; } } return $self->{__aggregates}->{$f}; } sub __aggregate_count__ { my $self = shift; my @members = $self->members; return scalar(@members); } sub __aggregate_min__ { my $self = shift; my $p = shift; my $min = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $min = $v if (!defined($min) || ($v < $min) || ($v lt $min)); } return $min; } sub __aggregate_max__ { my $self = shift; my $p = shift; my $max = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $max = $v if (!defined($max) || ($v > $max) || ($v gt $max)); } return $max; } sub __aggregate_sum__ { my $self = shift; my $p = shift; my $sum = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $sum += $v; } return $sum; } sub __related_set__ { my $self = $_[0]; my $property_name = $_[1]; my $bx1 = $self->rule; my $bx2 = $bx1->reframe($property_name); return $bx2->subject_class_name->define_set($bx2); } require Class::AutoloadCAN; Class::AutoloadCAN->import(); sub CAN { my ($class,$method,$self) = @_; if ($method =~ /^__aggregate_(.*)__/) { # prevent circularity issues since this actually calls ->can(); return; } my $member_class_name = $class; $member_class_name =~ s/::Set$//g; return unless $member_class_name; my $is_class_method = !ref($self); my $member_method_closure = $member_class_name->can($method); if ($is_class_method && $member_method_closure) { # We should only get here if the Set class has not implemented the method. # In which case we will delegate to the member class. return sub { my $self = shift; return $member_method_closure->($member_class_name, @_); }; } if ($member_method_closure) { my $member_class_meta = $member_class_name->__meta__; my $member_property_meta = $member_class_meta->property_meta_for_name($method); # regular property access if ($member_property_meta) { return sub { my $self = shift; if (@_) { Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable"); } my $rule = $self->rule; if ($rule->specifies_value_for($method)) { return $rule->value_for($method); } else { my @members = $self->members; my @values = map { $_->$method } @members; return @values if wantarray; return if not defined wantarray; Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray; return $values[0]; } }; } # set relaying with $s->foo_set->bar_set->baz_set; if (my ($property_name) = ($method =~ /^(.*)_set$/)) { return sub { shift->__related_set__($property_name, @_) } } # other method return sub { my $self = shift; if (@_) { Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable"); } my @members = $self->members; my @values = map { $_->$method } @members; return @values if wantarray; return if not defined wantarray; Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray; return $values[0]; }; } else { # a possible aggregation function # see if the method ___aggregate__ uses exists, and if so, delegate to __aggregate__ # TODO: delegate these to aggregation function modules instead of having them in this module my $aggregator = '__aggregate_' . $method . '__'; if ($self->can($aggregator)) { return sub { my $self = shift; my $f = $method; my @aggr_properties = @_; if (@aggr_properties) { $f .= '(' . join(',',@aggr_properties) . ')'; } return $self->__aggregate__({ f => $f, properties => \@aggr_properties }); }; } # set relaying with $s->foo_set->bar_set->baz_set; if (my ($property_name) = ($method =~ /^(.*)_set$/)) { return sub { shift->__related_set__($property_name, @_) } } } return; } 1; Html.pm100664023532023421 60012544604516 20406 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Html; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Set::View::Default::Html { is => 'UR::Object::View::Default::Html', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Json.pm100664023532023421 60012544604516 20413 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Json; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Set::View::Default::Json { is => 'UR::Object::View::Default::Json', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Text.pm100664023532023421 60012544604516 20426 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Set::View::Default::Text { is => 'UR::Object::View::Default::Text', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Xml.pm100664023532023421 57512544604516 20255 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Set::View::Default::Xml { is => 'UR::Object::View::Default::Xml', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Tag.pm100664023532023421 765112544604516 15221 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Tag; #TODO: update these to be UR::Value objects instead of some ancient hack =pod =head1 NAME UR::Object::Tag - Transitory attribute tags for a UR::Object at a given time. =head1 SYNOPSIS if (my @attribs = grep { $_->type eq 'invalid' } $obj->attribs()) { print $obj->display_name . " has invalid attributes. They are:\n"; for my $atrib (@attribs) { print join(",",$attrib->properties) . ":" . $attrib->desc . "\n"; } } Project H_NHF00 has invalid attributes, they are: project_subdirectory : Directory does not exist. target, status : Target cannot be null for projects with an active status. =head1 DESCRIPTION Objects of this class are created by create_attribs() on classes derived from UR::Object. They are retrieved by UR::Object->attribs(). =head1 INHERITANCE This class inherits from UR::ModuleBase. =head1 OBJECT METHODS =over 4 =item type A single-word description of the attribute which categorizes the attribute. Common attribute types are: =over 6 =item invalid Set when the object has invalid properties and cannot be saved. =item changed Set when the object is different than its "saved" version. =item hidden Set when the object has properties which should not be shown. =item editable Set when some part of the object is editable in the current context. =item warning Set when a warning about the state of the object is in effect. =item match Set when a search which is in effect matches this object's property(s). =item comment Set when this attribute is just an informational message. =back =item properties A list of properties to which the attribute applies. This is null when the attribute applies to the whole object, but typically returns one property name. Occasionally, it returns more than one property. Very rarely (currently never), the property may be in the form of an arrayref like: [ class_name, id, property_name ], in which case the property may actually be that of another related object. =item desc A string of text giving detail to the attribute. =back =head1 CLASS METHODS =over 4 =item create Makes a new UR::Object::Tag. =item delete Throws one away. =item filter Sets/gets a filter to be applied to all attribute lists returned in the application. This gives the application developer final veto power over expressed attributes in the app. In most cases, developers will write view components which use attributes, and will ignore them rather than plug-in at this low level to augment/mangle/supress. The filter will be given an object reference and a refrence to an array of attributes which are tentatively to be delivered for the object. =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION; # set up module use base qw(UR::ModuleBase); our (@EXPORT, @EXPORT_OK); @EXPORT = qw(); @EXPORT_OK = qw(); ##- use UR::Util; our %default_values = ( type => undef, properties => [], desc => undef ); UR::Util->generate_readwrite_methods(%default_values); *type_name = \&type; *property_names = \&properties; *description = \&description; sub create($@) { my ($class, @initial_prop) = @_; my $self = bless({%default_values,@initial_prop},$class); if (not ref($self->{properties}) eq 'ARRAY') { $self->{properties} = [ $self->{properties} ]; } return $self; } sub delete($) { UR::DeletedRef->bury($_[0]) } our $filter; sub filter { if (@_ > 1) { my $old = $filter; $filter = $_[1]; return $old; } return $filter; } sub __display_name__ { my $self = shift; my $desc = $self->desc; my $prefix = uc($self->type); my @properties = map { "'$_'" } $self->properties; my $prop_noun = scalar(@properties) > 1 ? 'properties' : 'property'; my $msg = "$prefix: $prop_noun " . join(', ', @properties) . ": $desc"; return $msg; } 1; __END__ =pod =back =head1 SEE ALSO UR::Object(3) =cut #$Header$ Type.pm100664023532023421 1237612544604516 15447 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Type; use warnings; use strict; require UR; # Used during bootstrapping. our @ISA = qw(UR::Object); our $VERSION = "0.44"; # UR $VERSION;; our @CARP_NOT = qw( UR::Object UR::Context UR::ModuleLoader Class::Autouse UR::BoolExpr ); # Most of the API for this module are legacy internals required by UR. use UR::Object::Type::InternalAPI; # This module implements define(), and most everything behind it. use UR::Object::Type::Initializer; # The methods used by the initializer to write accessors in perl. use UR::Object::Type::AccessorWriter; # The methods to extract/(re)create definition text in the module source file. use UR::Object::Type::ModuleWriter; # Present the internal definer as an external method sub define { shift->__define__(@_) } # For efficiency, certain hash keys inside the class cache property metadata # These go in this array, and are cleared when property metadata is mutated our @cache_keys; # This is the function behind $class_meta->properties(...) # It mimics the has-many object accessor, but handles inheritance # Once we have "isa" and "is-parent-of" operator we can do this with regular operators. push @cache_keys, '_properties'; sub _properties { my $self = shift; my $all = $self->{_properties} ||= do { # start with everything, as it's a small list my $map = $self->_property_name_class_map; my @all; for my $property_name (sort keys %$map) { my $class_names = $map->{$property_name}; my $class_name = $class_names->[0]; my $id = $class_name . "\t" . $property_name; my $property_meta = UR::Object::Property->get($id); unless ($property_meta) { Carp::confess("Failed to find property meta for $class_name $property_name?"); } push @all, $property_meta; } \@all; }; if (@_) { my ($bx, %extra) = UR::Object::Property->define_boolexpr(@_); my @matches = grep { $bx->evaluate($_) } @$all; if (%extra) { # Additional meta-properties on meta-properties are not queryable until we # put the UR::Object::Property into a private sub-class. # This will give us most of the functionality. for my $key (keys %extra) { my ($name,$op) = ($key =~ /(\w+)\s*(.*)/); unless (defined $self->{attributes_have}->{$name}) { die "unknown property $name used to query properties of " . $self->class_name; } if ($op and $op ne '==' and $op ne 'eq') { die "operations besides equals are not supported currently for added meta-properties like $name on class " . $self->class_name; } my $value = $extra{$key}; no warnings; @matches = grep { $_->can($name) and $_->$name eq $value } @matches; } } return if not defined wantarray; return @matches if wantarray; die "Matched multiple meta-properties, but called in scalar context!" . Data::Dumper::Dumper(\@matches) if @matches > 1; return $matches[0]; } else { @$all; } } sub property { if (@_ == 2) { # optimize for the common case my ($self, $property_name) = @_; my $class_names = $self->_property_name_class_map->{$property_name}; return unless $class_names and @$class_names; my $id = $class_names->[0] . "\t" . $property_name; return UR::Object::Property->get($id); } else { # this forces scalar context, raising an exception if # the params used result in more than one match my $one = shift->properties(@_); return $one; } } push @cache_keys, '_property_names'; sub property_names { my $self = $_[0]; my $names = $self->{_property_names} ||= do { my @names = sort keys %{ shift->_property_name_class_map }; \@names; }; return @$names; } push @cache_keys, '_property_name_class_map'; sub _property_name_class_map { my $self = shift; my $map = $self->{_property_name_class_map} ||= do { my %map = (); for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if (my $has = $class_meta->{has}) { for my $key (sort keys %$has) { my $classes = $map{$key} ||= []; push @$classes, $class_name; } } } \%map; }; return $map; } # The prior implementation of _properties() (behind ->properties()) # filtered out certain property meta. This is the old version. # The new version above will return one object per property name in # the meta ancestry. sub _legacy_properties { my $self = shift; if (@_) { my $bx = UR::Object::Property->define_boolexpr(@_); my @matches = grep { $bx->evaluate($_) } $self->property_metas; return if not defined wantarray; return @matches if wantarray; die "Matched multiple meta-properties, but called in scalar context!" . Data::Dumper::Dumper(\@matches) if @matches > 1; return $matches[0]; } else { $self->property_metas; } } 1; Type.pod100664023532023421 3627512544604516 15621 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object=pod =head1 NAME UR::Object::Type - a meta-class for any class or primitive type =head1 SYNOPSIS use UR; class MyClass { is => ['ParentClass1', 'ParentClass2'], id_by => [ id_prop1 => { is => 'Integer' }, id_prop2 => { is => 'String' }, ], has => [ property_a => { is => 'String' } property_b => { is => 'Integer', is_optional => 1 }, ], }; my $meta = MyClass->__meta__; my @parent_class_metas = $meta->parents(); # 2 meta objects, see UR::Object::Property my @property_meta = $meta->properties(); # N properties (4, +1 from UR::Object, +? from ParentClass1 and ParentClass2) $meta->is_abstract; $meta->... =head1 DESCRIPTION UR::Object::Type implements the class behind the central metadata in the UR class framework. It contains methods for introspection and manipulation of related class data. A UR::Object::Type object describes UR::Object, and also every subclass of UR::Object. =head1 INHERITANCE In addition to describing UR::Object an each of its subclasses, UR::Object::Type is _itself_ a subclass of L. This means that the same query APIs used for regular objects can be used for meta objects. UR::Object -> has-meta -> UR::Object::Type A | \ / \-----<- is-a <-------/ Further, new classes which generate a new UR::Objec::Type, also generate a new subclass for the meta-class. This means that each new class can have private meta methods, (ala Ruby). This means that extensions to a meta-class, apply to the meta-class of its derivatives. Regular Meta-Class Entity Singleton ------- ---------- Greyhound has-meta -> Greyhound::Type | | V V is-a is-a | | V V Dog has-meta -> Dog::Type | | V V is-a is-a | | V V Animal has-meta -> Animal::Type | | V V is-a is-a | | /-----------------\ V V V | UR::Object has-meta -> UR::Object::Type has-meta -/ A is-a | | \______________________/ =head1 CONSTRUCTORS =over 4 =item "class" class MyClass1 {}; class MyClass2 { is => 'MyClass1' }; class MyClass3 { is => ['Parent1','Parent2'], is_abstract => 1, is_transient => 1, has => [ qw/p1 p2 p3/ ], doc => 'woo hoo!' }; The primary constructor is not a method on this class at all. UR catches "class SOMENAME { ... }" and calls define() with the parameters. =item define my $class_obj = UR::Object::Type->define( class_name => 'MyClass', ... ); Register a class with the system. The given class_name must be unique within the application. As a side effect, a new Perl namespace will be created for the class's name, and methods will be injected into that namespace for any of the class properties. Other types of metadata objects will get created to manage the properties and relationships to other classes. See the L documentation for more information about the parameters C accepts. =item create my $class_obj = UR::Object::Type->create( class_name => 'Namespace::MyClass', ... ); Create a brand new class within an already existing UR namespace. C takes all the same parameters as C. Another side effect of create is that when the application commits its Context, a new Perl module will be created to implement the class, complete with a class definition. Applications will not normally use create(). =back =head1 PROPERTIES Each property has a method of the same name =head2 External API =over 4 =item class_name $name = $class_obj->class_name The name of the class. Class names are unique within a UR namespace and an application. This is symmetrical with $class_obj = $name->__meta__. =item properties @all = $class_obj->properties(); @some = $class_obj->properties( 'is => ['Text','Number'] 'doc like' => '%important%', 'property_name like' => 'someprefix_%', ); Access the related property meta-objects for all properties of this class. It includes the properties of any parent classes which are inherited by this class. See L for details. =item property $property_meta = $class_obj->property('someproperty'); The singular version of the above. A single argument, as usual, is treated as the remainder of the ID, and will select a property by name. =item namespace $namespace_name = $class_obj->namespace Returns the name of the class's UR namespace. =item doc $doc = $class_obj->doc A place to put general class-specific notes. =item data_source_id $ds_id = $class_obj->data_source_id The name of the external data source behind this class. Classes without data sources cannot be saved and exist only during the life of the application. data_source_id will resolve to an L id. =item table_name $table_name = $class_object->table_name For classes with data sources, this is the name of the table within that data source. This is usually a table in a relational database. At a basic level, it is a storage directive interpreted by the data_source, and may or may not related to a storage table at that level. =item is_abstract $bool = $class_obj->is_abstract A flag indicating if this is an abstract class. Abstract classes cannot have instances, but can be inherited by other classes. =item is_final $bool = $class_obj->is_final A flag indicating if this class cannot have subclasses. =item is_singleton $bool = $class_obj->is_singleton A flag indicating whether this is a singleton class. If true, the class will inherit from L. =item is_transactional $bool = $class_obj->is_transactional A flag indicating whether changes to this class's instances will be tracked. Non-transactional objecs do not change when an in-memory transaction rolls back. It is similar to the is_transient meta-property, which does the same for an individual property. =back =head2 Internal API These methods return data about how this class relates to other classes. =over 4 =item namespace_meta $ns_meta = $class_obj->namespace_meta Returns the L object with the class's namespace name. =item parent_class_names @names = $class_obj->parent_class_names Returns a list of the immediate parent classes. =item parent_class_metas @class_objs = $class_obj->parent_class_metas Returns a list of the class objects (L instances) of the immediate parent classes =item ancestry_class_names @names = $class_obj->ancestry_class_names Returns a list of all the class names this class inherits from, directly or indirectly. This list may have duplicate names if there is multiple inheritance in the family tree. =item ancestry_class_metas @class_objs = $class_obj->ancestry_class_metas Returns a list of the class objects for each inherited class. =item direct_property_names @names = $class_obj->direct_property_names Returns a list of the property names defined within this class. This list will not include the names of any properties inherited from parent classes unless they have been overridden. =item direct_property_metas @property_objs = $class_obj->direct_property_metas Returns a list of the L objects for each direct property name. =item ancestry_property_names @names = $class_obj->ancestry_property_names Returns a list of property names of the parent classes and their inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item ancestry_property_metas @property_objs = $class_obj->ancestry_property_metas; Returns a list of the L objects for each ancestry property name. =item all_property_names Returns a list of property names of the given class and its inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item all_property_metas @property_objs = $class_obj->all_property_metas; Returns a list of the L objects for each name returned by all_property_names. =item direct_id_property_names @names = $class_obj->direct_id_property_names Returns a list of the property names designated as "id" properties in the class definition. =item direct_id_property_metas @property_objs = $class_obj->direct_id_property_metas Returns a list of the L objects for each id property name. =item ancestry_id_property_names =item ancestry_id_property_metas =item all_id_property_names =item all_id_property_metas @names = $class_obj->ancestry_id_property_names; @property_objs = $class_obj->ancestry_id_property_metas; @names = $class_obj->all_id_property_names; @property_objs = $class_obj->all_id_property_metas; Returns the property names or L objects for either the parent classes and their inheritance heirarchy, or for the given class and all of its inheritance heirarchy. The lists may include duplicates if properties are overridden somewhere in the heirarchy. =item unique_property_set_hashref $constraints = $class_obj->unique_property_set_hashref Return a hashref describing the unique constraints on the given class. The keys of $constraint are constraint names, and the values are listrefs of property names that make up the unique constraint. =item add_unique_constraint $class_obj->add_unique_constraint($constraint_name, @property_name_list) Add a unique constraint to the given class. It is an exception if the given $constraint_name already exists as a constraint on this class or its parent classes. =item remove_unique_constraint $class_obj->remove_unique_constraint($constraint_name) Remove a unique constraint from the given class. It is an exception if the given constraint name does not exist. =item ancestry_table_names =item all_table_names @names = $class_obj->ancestry_table_names Returns a list of table names in the class's inheritance heirarchy. =item direct_column_names Returns a list of column names for each direct property meta. Classes with data sources and table names will have properties with column names. =item direct_id_column_names Returns a list of ID column names for each direct property meta. =item direct_columnless_property_names =item direct_columnless_property_metas =item ancestry_columnless_property_names =item ancestry_columnless_property_metas =item all_columnless_property_names =item all_columnless_property_metas Return lists of property meta objects and their names for properties that have no column name. =back =head1 METHODS =over 4 =item property_meta_for_name $property_obj = $class_obj->property_meta_for_name($property_name); Return the L object in the class's inheritance hierarchy with the given name. If the property name has been overridden somewhere in the hierarchy, then it will return the property object most specific to the class. =item id_property_sorter $subref = $class_obj->id_property_sorter; @sorted_objs = sort $subref @unsorted_objs; Returns a subroutine reference that can be used to sort object instances of the class. The subref is able to handle classes with multiple ID properties, and mixes of numeric and non-numeric data and data types. =item autogenerate_new_object_id This method is called whenever new objects of the given class are created through Ccreate()>, and not all of their ID properties were specified. UR::Object::Type has an implementation used by default, but other classes can override this if they need special handling. =item singular_accessor_name_for_is_many_accessor $property_name = $class_obj->singular_accessor_name_for_is_many_accessor($is_many_name); For is_many properties, returns the name of the singular accessor. Usually, this the singular version of the plural, is_many property's name. The singular accessor accepts key/value pairs as arguments, which are used to filter the results of the is_many accessor. For example, the singular for the 'cars' accessor is 'car'. Returns a false value if the given property name does not exist or is not is_many. =item iterator_accessor_name_for_is_many_accessor $iter_name = $class_obj->iterator_accessor_name_for_is_many_accessor($is_many_name); Returns the accessor name used to get the L that corresponds with the is_many accessor. For example, the iterator for the 'cars' accessor is 'car_iterator'. Returns a false value if the given property name does not exist or is not is_many. =item set_accessor_name_for_is_many_accessor $set_name = $class_obj->set_accessor_name_for_is_many_accessor($is_many_name); Returns the accessor name used to get the L that corresponds with the is_many accessor. For example, the set for the 'cars' accessor is 'car_set'. Returns a false value if the given property name does not exist or is not is_many. =item rule_accessor_name_for_is_many_accessor $rule_name = $class_obj->rule_accessor_name_for_is_many_accessor($is_many_name); Returns the accessor name used to get the L that corresponds with the is_many accessor. For example, the rule for the 'cars' accessor is '__car_rule'. Returns a false value if the given property name does not exist or is not is_many. =item arrayref_accessor_name_for_is_many_accessor $arrayref_name = $class_obj->arrayref_accessor_name_for_is_many_accessor($is_many_name); Returns the accessor name used to get the arrayref of objects that corresponds with the is_many accessor. For example, the arrayref for the 'cars' accessor is 'car_arrayref'. Returns a false value if the given property name does not exist or is not is_many. =item adder_name_for_is_many_accessor $adder_name = $class_obj->adder_name_for_is_many_accessor($is_many_name); Returns the method name used to get the adder method that corresponds with the is_many accessor. For example, the adder for the 'cars' accessor is 'add_car'. Returns a false value if the given property name does not exist or is not is_many. =item remover_name_for_is_many_accessor $remover_name = $class_obj->remover_name_for_is_many_accessor($is_many_name); Returns the method name used to get the remover method that corresponds with the is_many accessor. For example, the adder for the 'cars' accessor is 'remove_car'. Returns a false value if the given property name does not exist or is not is_many. =back =head1 SEE ALSO L, L, L, L =cut AccessorWriter.pm100664023532023421 23664012544604516 20430 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type package UR::Object::Type::AccessorWriter; package UR::Object::Type; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; #use warnings FATAL => 'all'; use Carp (); use Sub::Name (); use Sub::Install (); use List::Util; sub mk_rw_accessor { my ($self, $class_name, $accessor_name, $column_name, $property_name, $is_transient) = @_; $property_name ||= $accessor_name; my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $property_name }; my $new = $_[1]; # The accessors may compare undef and an empty # string. For speed, we turn warnings off rather # than add extra code to make the warning disappear. my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $_[0]->{ $property_name } = $new; $_[0]->__signal_change__( $property_name, $old, $new ) unless $is_transient; # FIXME is $is_transient right here? Maybe is_volatile instead (if at all)? } return $new; } my $r = eval { $_[0]->{ $property_name } }; if ($@) { Carp::confess("Exception while processing property $property_name: $@"); } return $r; }; if (_class_is_singleton($class_name)) { my $basic_accessor = $accessor; $accessor = sub { shift->_singleton_object->$basic_accessor(@_); }; } Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_ro_accessor { my ($self, $class_name, $accessor_name, $column_name, $property_name) = @_; $property_name ||= $accessor_name; my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $property_name}; my $new = $_[1]; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { Carp::croak("Cannot change read-only property $accessor_name for class $class_name!" . " Failed to update " . $_[0]->__display_name__ . " property: $property_name from $old to $new"); } return $new; } return $_[0]->{ $property_name }; }; if (_class_is_singleton($class_name)) { my $basic_accessor = $accessor; $accessor = sub { shift->_singleton_object->$basic_accessor(@_); }; } Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub _class_is_singleton { my $class_name = shift; return grep { $_->isa('UR::Singleton') } @{ $class_name->__meta__->{is} }; } sub mk_id_based_flex_accessor { my ($self, $class_name, $accessor_name, $id_by, $r_class_name, $where, $id_class_by) = @_; unless (ref($id_by)) { $id_by = [ $id_by ]; } my $id_resolver; my $id_decomposer; my @id; my $id; my $full_name = join( '::', $class_name, $accessor_name ); my $concrete_r_class_name = $r_class_name; my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; if (@_ == 1) { # This one is to support syntax like this # $cd->artist($different_artist); # to switch which artist object this cd points to my $object_value = shift; if ($id_class_by and not ref $object_value) { # when we have an id-class-by accessor and get a primitive, store it as a UR::Value $object_value = UR::Value->get($object_value); } if (defined $object_value) { if ($id_class_by) { $concrete_r_class_name = ($object_value->can('class') ? $object_value->class : ref($object_value)); $id_decomposer = undef; $id_resolver = undef; $self->$id_class_by($concrete_r_class_name); } elsif (! Scalar::Util::blessed($object_value) and ! $object_value->can('id')) { Carp::croak("Can't call method \"id\" without a package or object reference. Expected an object as parameter to '$accessor_name', not the value '$object_value'"); } my $r_class_meta = eval { $concrete_r_class_name->__meta__ }; unless ($r_class_meta) { Carp::croak("Can't get metadata for class $concrete_r_class_name. Is it a UR class?"); } $id_decomposer ||= $r_class_meta->get_composite_id_decomposer; @id = $id_decomposer->($object_value->id); if (@$id_by == 1) { my $id_property_name = $id_by->[0]; $self->$id_property_name($object_value->id); } else { @id = $id_decomposer->($object_value->id); Carp::croak("Cannot alter value for '$accessor_name' on $class_name: The passed-in object of type " . $object_value->class . " has " . scalar(@id) . " id properties, but the accessor '$accessor_name' has " . scalar(@$id_by) . " id_by properties"); for my $id_property_name (@$id_by) { $self->$id_property_name(shift @id); } } } else { if ($id_class_by) { $self->$id_class_by(undef); } for my $id_property_name (@$id_by) { $self->$id_property_name(undef); } } return $object_value; } else { if ($id_class_by) { $concrete_r_class_name = $self->$id_class_by; $id_decomposer = undef; $id_resolver = undef; return unless $concrete_r_class_name; } unless ($id_resolver) { my $concrete_r_class_meta = UR::Object::Type->get($concrete_r_class_name); unless ($concrete_r_class_meta) { Carp::croak("Can't resolve value for '$accessor_name' on class $class_name id '".$self->id . "': No class metadata for value '$concrete_r_class_name' referenced as property '$id_class_by'"); } $id_resolver = $concrete_r_class_meta->get_composite_id_resolver; } # eliminate the old map{} because of side effects with $_ # when the id_by property happens to be calculated #@id = map { $self->$_ } @$id_by; @id=(); for my $property_name (@$id_by) { # no implicit topic my $value = $self->$property_name; # scalar context push @id, $value; } $id = $id_resolver->(@id); return if not defined $id; if ($concrete_r_class_name eq 'UR::Object') { Carp::carp("Querying by using UR::Object class is deprecated."); } if ($concrete_r_class_name->isa("UR::Value")) { return $id; } else { if (@_ || $where) { # There were additional params passed in return $concrete_r_class_name->get(id => $id, @_, @$where); } else { return $concrete_r_class_name->get($id); } } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_id_based_object_accessor { my ($self, $class_name, $accessor_name, $id_by, $r_class_name, $where, $id_class_by) = @_; unless (ref($id_by)) { $id_by = [ $id_by ]; } my $id_resolver; my $id_decomposer; my @id; my $id; my $full_name = join( '::', $class_name, $accessor_name ); my $concrete_r_class_name = $r_class_name; my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; if (@_ == 1) { # This one is to support syntax like this # $cd->artist($different_artist); # to switch which artist object this cd points to my $object_value = shift; if (defined $object_value) { if ($id_class_by) { $concrete_r_class_name = ($object_value->can('class') ? $object_value->class : ref($object_value)); $id_decomposer = undef; $id_resolver = undef; $self->$id_class_by($concrete_r_class_name); } elsif (! Scalar::Util::blessed($object_value) and ! $object_value->can('id')) { Carp::croak("Can't call method \"id\" without a package or object reference. Expected an object as parameter to '$accessor_name', not the value '$object_value'"); } my $r_class_meta = eval { $concrete_r_class_name->__meta__ }; unless ($r_class_meta) { Carp::croak("Can't get metadata for class $concrete_r_class_name. Is it a UR class?"); } $id_decomposer ||= $r_class_meta->get_composite_id_decomposer; @id = $id_decomposer->($object_value->id); if (@$id_by == 1) { my $id_property_name = $id_by->[0]; $self->$id_property_name($object_value->id); } else { @id = $id_decomposer->($object_value->id); Carp::croak("Cannot alter value for '$accessor_name' on $class_name: The passed-in object of type " . $object_value->class . " has " . scalar(@id) . " id properties, but the accessor '$accessor_name' has " . scalar(@$id_by) . " id_by properties"); for my $id_property_name (@$id_by) { $self->$id_property_name(shift @id); } } } else { if ($id_class_by) { $self->$id_class_by(undef); } for my $id_property_name (@$id_by) { $self->$id_property_name(undef); } } return $object_value; } else { if ($id_class_by) { $concrete_r_class_name = $self->$id_class_by; $id_decomposer = undef; $id_resolver = undef; return unless $concrete_r_class_name; } unless ($id_resolver) { my $concrete_r_class_meta = UR::Object::Type->get($concrete_r_class_name); unless ($concrete_r_class_meta) { Carp::croak("Can't resolve value for '$accessor_name' on class $class_name id '".$self->id . "': No class metadata for value '$concrete_r_class_name' referenced as property '$id_class_by'"); } $id_resolver = $concrete_r_class_meta->get_composite_id_resolver; } @id=(); for my $property_name (@$id_by) { # no implicit topic my $value = $self->$property_name; # scalar context push @id, $value; } $id = @id > 1 ? $id_resolver->(@id) : $id[0]; return if not defined $id; if ($concrete_r_class_name eq 'UR::Object') { Carp::carp("Querying by using UR::Object class is deprecated."); } if (@_ || $where) { # There were additional params passed in return $concrete_r_class_name->get(id => $id, @_, @$where); } else { return $concrete_r_class_name->get($id); } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub _resolve_bridge_logic_for_indirect_property { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where) = @_; my $bridge_collector = sub { my $self = shift; my @results = $self->$via(@$where); # Indirect has one properties must return a single undef value for an empty result, even in list context. return if @results == 1 and not defined $results[0]; return @results; }; my $bridge_crosser = sub { my $bridges = shift; return map { $_->$to(@_) } @$bridges; }; return($bridge_collector, $bridge_crosser) if ($UR::Object::Type::bootstrapping); # bail out and use the default subs if any of these fail my ($my_class_meta, $my_property_meta, $via_property_meta, $to_property_meta); $my_class_meta = $class_name->__meta__; $my_property_meta = $my_class_meta->property_meta_for_name($accessor_name) if ($my_class_meta); $via_property_meta = $my_class_meta->property_meta_for_name($via) if ($my_class_meta); $to_property_meta = $my_property_meta->to_property_meta() if ($my_property_meta); if (! $my_class_meta || ! $my_property_meta || ! $via_property_meta || ! $to_property_meta) { # Something didn't link right, use the default methods return ($bridge_collector, $bridge_crosser); } if ($my_property_meta->is_delegated and $my_property_meta->is_many and $via_property_meta->is_many and $via_property_meta->reverse_as and $via_property_meta->data_type and $via_property_meta->data_type->isa('UR::Object') ) { my $bridge_class = $via_property_meta->data_type; my @via_join_properties = eval { $via_property_meta->get_property_name_pairs_for_join }; if (! @via_join_properties) { # this can happen if the properties aren't linked together as expected. # For example, a property involved in a many-to-many relationship, but is # defined as a one-to-many with reverse_as. return ($bridge_collector, $bridge_crosser); } my (@my_join_properties,@their_join_properties); for (my $i = 0; $i < @via_join_properties; $i++) { ($my_join_properties[$i], $their_join_properties[$i]) = @{ $via_join_properties[$i] }; } my(@where_properties, @where_values, %bridge_meta_params); if ($where or $via_property_meta->where) { my @collected_where; @collected_where = @$where if ($where); push @collected_where, @{ $via_property_meta->where } if ($via_property_meta->where); while (@collected_where) { my $where_property = shift @collected_where; my $where_value = shift @collected_where; if (UR::BoolExpr::Util::is_meta_param($where_property)) { $bridge_meta_params{$where_property} = $where_value; } else { if (ref($where_value) eq 'HASH' and $where_value->{'operator'}) { $where_property .= ' ' .$where_value->{'operator'}; $where_value = $where_value->{'value'}; } push @where_properties, $where_property; push @where_values, $where_value; } } } my $bridge_template = UR::BoolExpr::Template->resolve($bridge_class, @their_join_properties, @where_properties, %bridge_meta_params); $bridge_collector = sub { my $self = shift; my @my_values = map { $self->$_} @my_join_properties; my $bx = $bridge_template->get_rule_for_values(@my_values, @where_values); return $bridge_class->get($bx); }; if ($to_property_meta->is_delegated and my $doubly_deledated_bridge_crosser = _resolve_bridge_crosser_for_doubly_delegated_property($to_property_meta, \%bridge_meta_params) ) { $bridge_crosser = $doubly_deledated_bridge_crosser; } } return ($bridge_collector, $bridge_crosser); } sub _make_results_sorter_for_doubly_delegated_bridge_crosser { my($bridges, $bridge_linker, $results_linker) = @_; my $rank = 0; my %bridge_rankings = map { $bridge_linker->() => $rank++ } @$bridges; return sub { my $results = shift; return map { $_->[1] } sort { $bridge_rankings{ $a->[0] } <=> $bridge_rankings{ $b->[0] } } map { [ $results_linker->(), $_ ] } @$results; }; } sub _resolve_bridge_crosser_for_doubly_delegated_property { my($to_property_meta, $bridge_meta_params) = @_; # This property's value is doubly delegated. The simple thing to # do is to collect the bridge objects, then call the second # delegation method on each bridge in a loop to collect the final # results, which may trigger one query per result. Depending on # the type of delegation, the final results can be collected with # one query my($result_class_resolver, $bridge_linking_properties, $final_result_property_name, $result_filtering_property); if ($to_property_meta->via) { # bridges through another via-to property my $second_via_property_meta = $to_property_meta->via_property_meta; my $final_class_name = $second_via_property_meta->data_type; if ($final_class_name and $final_class_name ne 'UR::Value' and $final_class_name->isa('UR::Object')) { if ( 1 == (my @via2_join_properties = $second_via_property_meta->get_property_name_pairs_for_join)) { $bridge_linking_properties = [ $via2_join_properties[0]->[0] ]; $result_filtering_property = $via2_join_properties[0]->[1]; $result_class_resolver = sub { $final_class_name }; $final_result_property_name = $to_property_meta->to; } } } elsif ($to_property_meta->id_by) { $bridge_linking_properties = $to_property_meta->id_by; $result_filtering_property = 'id'; if ($to_property_meta->id_class_by) { # Bridging through an 'id_class_by' property # bucket the bridge items by the result class and do a get for # each of those classes with a listref of IDs my $result_class_resolving_property = $to_property_meta->id_class_by; $result_class_resolver = sub { shift->$result_class_resolving_property }; } else { # Bridging through a regular id-by property my $result_class = $to_property_meta->data_type; $result_class_resolver = sub { $result_class }; } } elsif ($to_property_meta->reverse_as) { if (1 == (my @reverse_as_join_properties = $to_property_meta->get_property_name_pairs_for_join)) { $bridge_linking_properties = [ map { $_->[0] } @reverse_as_join_properties ]; $result_filtering_property = $reverse_as_join_properties[0]->[1]; my $result_class = $to_property_meta->data_type; $result_class_resolver = sub { $result_class }; } } if ($result_class_resolver) { my $linking_id_value_for_bridge = do { my %composite_id_resolver_for_class; sub { my $bridge = shift; my @id = map { $bridge->$_ } @$bridge_linking_properties; my $result_class = $result_class_resolver->($bridge); my $id_resolver = $composite_id_resolver_for_class{ $result_class } ||= $result_class->__meta__->get_composite_id_resolver; return $id_resolver->(@id); }; }; return sub { my $bridges = shift; my %result_class_names_and_ids; foreach my $bridge ( @$bridges ) { my $result_class = $result_class_resolver->($bridge); $result_class_names_and_ids{$result_class} ||= []; my $id = $linking_id_value_for_bridge->($bridge); push @{ $result_class_names_and_ids{ $result_class } }, $id; } my @results; foreach my $result_class ( keys %result_class_names_and_ids ) { if($result_class->isa('UR::Value')) { #can't group queries together for UR::Values push @results, map { $result_class->get($result_filtering_property => $_, @_) } @{$result_class_names_and_ids{$result_class}}; } else { push @results, $result_class->get($result_filtering_property => $result_class_names_and_ids{$result_class}, @_); } } if ($bridge_meta_params->{'-order'} || $bridge_meta_params->{'-order_by'}) { my $results_sorter = _make_results_sorter_for_doubly_delegated_bridge_crosser( $bridges, sub { return $linking_id_value_for_bridge->($_) }, sub { $_->id } ); @results = $results_sorter->(\@results); } @results = map { $_->$final_result_property_name } @results if ($to_property_meta->via); return @results; }; } return; } sub _is_assignment_value { return ( @_ == 1 and not (ref($_[0]) and Scalar::Util::blessed($_[0]) and $_[0]->isa("UR::BoolExpr")) ); } sub mk_indirect_ro_accessor { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where) = @_; my @where = ($where ? @$where : ()); my $full_name = join( '::', $class_name, $accessor_name ); my $filterable_accessor_name = 'get_' . $accessor_name; # FIXME we need a better name for my $filterable_full_name = join( '::', $class_name, $filterable_accessor_name ); # This is part of an experimental refactoring of indirect accessors. The goal is to # get rid of all the special cases inside of _resolve_bridge_logic_for_indirect_property() # and do the right thing with the Join data my (@collectors, @crossers); my $accessor2 = Sub::Name::subname $full_name.'_new' => sub { my $self = shift; Carp::croak("Assignment value passed to read-only indirect accessor $accessor_name for class $class_name") if @_ and _is_assignment_value(@_); if ($class_name =~ m/^UR::/) { # Some methods will recurse into here if called on a UR::* class (especially # UR::BoolExpr), so do the dumb but safe thing my $bridge_collector = sub { my $self = shift; my @results = $self->$via(@$where); # Indirect has one properties must return a single undef value for an empty result, even in list context. return if @results == 1 and not defined $results[0]; return @results; }; #TODO: move this crosser closure logic down and get rid of the closure my @filter = @_; my $bridge_crosser = sub { return map { $_->$to(@filter) } @_ }; my @bridges = $bridge_collector->($self); return unless @bridges; return $self->context_return(@bridges) if ($to eq '-filter'); my @results = $bridge_crosser->(@bridges); return $self->context_return(@results); } unless (@collectors) { require List::MoreUtils; my $prop_meta = $class_name->__meta__->property_meta_for_name($accessor_name); my @join_list = $prop_meta->_resolve_join_chain(); foreach my $join ( @join_list ) { my @source_property_names = @{$join->{source_property_names}}; my $collector = sub { my @list = grep { defined && length } map { my $o = $_; map { $o->$_ } @source_property_names} @_; return @list == 1 ? $list[0] : \@list; }; push @collectors, $collector; my $foreign_class = $join->{foreign_class}; my $crosser; if (! $foreign_class->isa('UR::Value')) { my @foreign_property_names = @{$join->{foreign_property_names}}; $crosser = sub { my @get_params = List::MoreUtils::pairwise { $a => $b } @foreign_property_names, @_; return $foreign_class->get(@get_params); }; } push @crossers, $crosser; } } my @working = ($self); # This can probably be rewritten with List::Util::reduce for (my $i = 0; $i < @collectors; $i++) { last unless @working; my @working = $collectors[$i]->(@working); next unless $crossers[$i]; @working = $crossers[$i]->(@working); } $self->context_return(@working); }; #Sub::Install::reinstall_sub({ # into => $class_name, # as => $accessor_name.'_new', # code => $accessor2, #}); my($bridge_collector, $bridge_crosser); my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; Carp::croak("Assignment value passed to read-only indirect accessor $accessor_name for class $class_name") if @_ == 1 and _is_assignment_value(@_); unless ($bridge_collector) { ($bridge_collector, $bridge_crosser) = $ur_object_type->_resolve_bridge_logic_for_indirect_property($class_name, $accessor_name, $via, $to, \@where); } my @bridges = $bridge_collector->($self); return unless @bridges; return $self->context_return(@bridges) if ($to eq '-filter'); my @results = $bridge_crosser->(\@bridges, @_); $self->context_return(@results); }; unless ($accessor_name) { Carp::croak("No accessor name specified for read-only indirect accessor $accessor_name for class $class_name"); } Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); my $r_class_name; my $r_class_name_resolver = sub { return $r_class_name if $r_class_name; my $linking_property = UR::Object::Property->get(class_name => $class_name, property_name => $via); unless ($linking_property->data_type) { Carp::croak "Property ${class_name}::${accessor_name}: via refers to a property with no data_type. Can't process filter"; } my $final_property = UR::Object::Property->get(class_name => $linking_property->data_type, property_name => $to); unless ($final_property->data_type) { Carp::croak "Property ${class_name}::${accessor_name}: to refers to a property with no data_type. Can't process filter"; } $r_class_name = $final_property->data_type; }; my $filterable_accessor = Sub::Name::subname $filterable_full_name => sub { my $self = shift; my @results = $self->$accessor_name(); if (@_) { my $rule; if (@_ == 1 and ref($_[0]) and $_[0]->isa('UR::BoolExpr')) { $rule = shift; } else { $r_class_name ||= $r_class_name_resolver->(); $rule = UR::BoolExpr->resolve_normalized($r_class_name, @_); } @results = grep { $rule->evaluate($_) } @results; } $self->context_return(@results); }; Sub::Install::reinstall_sub({ into => $class_name, as => $filterable_accessor_name, code => $filterable_accessor, }); } sub mk_indirect_rw_accessor { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where, $singular_name, $property_name) = @_; $property_name ||= $accessor_name; my @where = ($where ? @$where : ()); my $full_name = join( '::', $class_name, $accessor_name ); my $update_strategy; # defined the first time we "set" a value through this my $adder; my $via_property_meta; my $r_class_name; my $is_many; my $resolve_update_strategy = sub { unless (defined $update_strategy) { # Resolve the strategy. We need to figure out if $to # refers to an id-property. This is only called once, when the # accessor is first used. # If we reference a remote object, and go to one of its id properties # we must do a delete/create instead of property change. Note that # this is only allowed when the remote object has no direct properties # which are not id properties. my $my_property_meta = $class_name->__meta__->property_meta_for_name($property_name); unless ($my_property_meta) { Carp::croak("Failed to find property meta for '$property_name' on class $class_name"); } $is_many = $my_property_meta->is_many; $via_property_meta ||= $class_name->__meta__->property_meta_for_name($via); unless ($via_property_meta) { Carp::croak("Failed to find property metadata for via property '$via' while resolving property '$property_name' on class $class_name"); } $r_class_name ||= $via_property_meta->data_type; unless ($r_class_name) { Carp::croak("Cannot resolve property '$property_name' on class $class_name: It is via property '$via' which has no data_type"); } my $r_class_meta = $r_class_name->__meta__; unless ($r_class_meta) { Carp::croak("Cannot resolve property '$property_name' on class $class_name: It is via property '$via' with data_type $r_class_name which is not a valid class name"); } $adder = "add_" . $via_property_meta->singular_name; if ($my_property_meta->_involves_id_property) { $update_strategy = 'delete-create' } else { $update_strategy = 'change'; } } return $update_strategy; }; my ($bridge_collector, $bridge_crosser); my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; unless ($bridge_collector) { ($bridge_collector, $bridge_crosser) = $ur_object_type->_resolve_bridge_logic_for_indirect_property($class_name, $accessor_name, $via, $to, \@where); } my @bridges = $bridge_collector->($self); if ( @_ == 1 and _is_assignment_value(@_) ) { $resolve_update_strategy->() unless (defined $update_strategy); if ($update_strategy eq 'change') { if (@bridges == 0) { #print "adding via $adder @where :::> $to @_\n"; @bridges = eval { $self->$adder(@where, $to => $_[0]) }; if ($@) { my $r_class_meta = $r_class_name->__meta__; my $property_meta = $r_class_meta->property($to); if ($property_meta) { # Re-throw the original exception die $@; } else { Carp::croak("Couldn't create a new object through indirect property " . "'$accessor_name' on $class_name. 'to' is $to which is not a property on $r_class_name."); } } #WAS > Carp::confess("Cannot set $accessor_name on $class_name $self->{id}: property is via $via which is not set!"); } elsif (@bridges > 1) { Carp::croak("Cannot set '$accessor_name' on $class_name id '$self->{id}': multiple instances of '$via' found, via which the property is set"); } #print "updating $bridges[0] $to to @_\n"; return $bridges[0]->$to(@_); } elsif ($update_strategy eq 'delete-create') { if (@bridges > 1) { Carp::croak("Cannot set '$accessor_name' on $class_name $self->{id}: multiple instances of '$via' found, via which the property is set"); } else { if (@bridges) { #print "deleting $bridges[0]\n"; $bridges[0]->delete; } #print "adding via $adder @where :::> $to @_\n"; @bridges = $self->$adder(@where, $to => $_[0]); unless (@bridges) { Carp::croak("Failed to add bridge for '$accessor_name' on $class_name if '$self->{id}': method $adder returned false"); } } } } if (not defined $is_many) { $resolve_update_strategy->(); } if ($is_many) { return unless @bridges; my @results = $bridge_crosser->(\@bridges, @_); $self->context_return(@results); } else { return undef unless @bridges; my @results = map { $_->$to(@_) } @bridges; $self->context_return(@results); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); if ($singular_name) { # True if we're defining an is_many indirect property # Add my $via_adder; my $adder_method_name = 'add_' . $singular_name; if ($class_name->can($adder_method_name)) { $adder_method_name = '__' . $adder_method_name; } my $adder_method = Sub::Name::subname $class_name . '::' . $adder_method_name => sub { my($self) = shift; $resolve_update_strategy->() unless (defined $update_strategy); unless (defined $via_adder) { $via_adder = "add_" . $via_property_meta->singular_name; } # By default, a single value will come in which is the remote value # we just add the appropriate property name to it. If multiple # values come in we trust the caller to be giving additional params. if (@_ == 1) { unshift @_, $to; } $self->$via_adder(@where,@_); }; Sub::Install::reinstall_sub({ into => $class_name, as => $adder_method_name, code => $adder_method, }); # Remove my $via_remover; my $remover_method_name = 'remove_' . $singular_name; if ($class_name->can($remover_method_name)) { $remover_method_name = '__' . $remover_method_name; } my $remover_method = Sub::Name::subname $class_name . '::' . $remover_method_name => sub { my($self) = shift; $resolve_update_strategy->() unless (defined $update_strategy); unless (defined $via_remover) { $via_remover = "remove_" . $via_property_meta->singular_name; } # By default, a single value will come in which is the remote value # we just remove the appropriate property name to it. If multiple # values come in we trust the caller to be giving removeitional params. if (@_ == 1) { unshift @_, $to; } $self->$via_remover(@where,@_); }; Sub::Install::reinstall_sub({ into => $class_name, as => $remover_method_name, code => $remover_method, }); } } sub mk_calculation_accessor { my ($self, $class_name, $accessor_name, $calculation_src, $calculate_from, $params, $is_cached, $column_name) = @_; my $accessor; my @src; if (not defined $calculation_src or $calculation_src eq '') { $accessor = \&{ $class_name . '::' . $accessor_name }; unless ($accessor) { Carp::croak "$accessor_name not defined in $class_name! Define it, or specify a calculate => sub{} or calculate => \$perl_src in the class definition."; } } elsif (ref($calculation_src) eq 'CODE') { $accessor = sub { my $self = shift; if (@_) { Carp::croak("$class_name $accessor_name is a read-only property derived from @$calculate_from"); } return $calculation_src->(map { $self->$_ } @$calculate_from); }; } elsif ($calculation_src =~ /^[^\:\W]+$/) { # built-in formula like 'sum' or 'product' my $module_name = "UR::Object::Type::AccessorWriter::" . ucfirst(lc($calculation_src)); eval "use $module_name"; die $@ if $@; @src = ( "sub ${class_name}::${accessor_name} {", 'my $self = $_[0];', "${module_name}->calculate(\$self, [" . join(",", map { "'$_'" } @$calculate_from) . "], \@_)", '}' ); } else { @src = ( "sub ${class_name}::${accessor_name} {", ($params ? 'my ($self,%params) = @_;' : 'my $self = $_[0];'), (map { "my \$$_ = \$self->$_;" } @$calculate_from), ($params ? (map { "my \$$_ = delete \$params{'$_'};" } @$params) : ()), $calculation_src, '}' ); } if (!$accessor) { if (@src) { my $src = join("\n",@src); #print ">>$src<<\n"; eval $src; if ($@) { Carp::croak "ERROR IN CALCULATED PROPERTY SOURCE: $class_name $accessor_name\n$@\n"; } $accessor = \&{ $class_name . '::' . $accessor_name }; unless ($accessor) { Cqrp::confess("Failed to generate code body for calculated property ${class_name}::${accessor_name}!"); } } else { Carp::croak "Error implementing calcuation accessor for $class_name $accessor_name!"; } } if ($accessor and $is_cached) { # Wrap the already-compiled accessor in another function to memoize the # result and save the data into the object my $calculator_sub = $accessor; $accessor = sub { if (@_ > 1) { Carp::croak("Cannot change property $accessor_name for class $class_name: cached calculated properties are read-only"); } unless (exists $_[0]->{$accessor_name}) { $_[0]->{$accessor_name} = $calculator_sub->(@_); } return $_[0]->{$accessor_name}; }; # Make a method to clear the cached value and force another calculation my $invalidator_name; ($invalidator_name = $accessor_name) =~ s/^_+//; $invalidator_name = "__invalidate_${invalidator_name}__"; Sub::Install::reinstall_sub({ into => $class_name, as => $invalidator_name, code => sub { delete $_[0]->{$accessor_name} }, }); } my $full_name = join( '::', $class_name, $accessor_name ); $accessor = Sub::Name::subname $full_name => $accessor; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); return $accessor; } sub mk_dimension_delegate_accessors { my ($self, $accessor_name, $ref_class_name, $non_id_properties, $other_accessor_name, $is_transient) = @_; # Like mk_rw_accessor, but knows that this accessor is a foreign # key to a dimension table, and configures additional accessors. # Also makes this accessor "smart", to resolve the dimension # id only when needed. # Make EAV-like accessors for all of the remote properties my $class_name = $self->class_name; my $full_name = join( '::', $class_name, $other_accessor_name ); my $other_accessor = Sub::Name::subname $full_name => sub { my $self = shift; my $delegate_id = $self->{$accessor_name}; if (defined($delegate_id)) { # We're currently delegating. my $delegate = $ref_class_name->get($delegate_id); if (not @_) { # A simple get. Delegate. return $delegate->$other_accessor_name(@_); } else { # We're setting a value. # Switch from delegating to local access. # We'll switch back next-time the dimension ID # is actually requested by its accessor # (farther below). my $old = $delegate->$other_accessor_name; my $new = shift; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $self->{$accessor_name} = undef; for my $property (@$non_id_properties) { if ($property eq $other_accessor_name) { # set the value locally $self->{$property} = $new; } else { # grab the data from the (now previous) delegate $self->{$property} = $delegate->$property; } } $self->__signal_change__( $other_accessor_name, $old, $new ) unless $is_transient; return $new; } } } else { # We are not currently delegating. if (@_) { # set my $old = $self->{ $other_accessor_name }; my $new = shift; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $self->{ $other_accessor_name } = $new; $self->__signal_change__( $other_accessor_name, $old, $new ) unless $is_transient; } return $new; } else { # get return $self->{ $other_accessor_name }; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $other_accessor_name, code => $other_accessor, }); } sub mk_dimension_identifying_accessor { my ($self, $accessor_name, $ref_class_name, $non_id_properties, $is_transient) = @_; # Like mk_rw_accessor, but knows that this accessor is a foreign # key to a dimension table, and configures additional accessors. # Also makes this accessor "smart", to resolve the dimension # id only when needed. # Make EAV-like accessors for all of the remote properties my $class_name = $self->class_name; # Make the actual accessor for the id_by property my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $accessor_name }; my $new = $_[1]; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $_[0]->{ $accessor_name } = $new; $_[0]->__signal_change__( $accessor_name, $old, $new ) unless $is_transient; } return $new; } if (not defined $_[0]->{ $accessor_name }) { # Resolve an ID for the current set of values # Switch to delegating to that object. my %params; my $self = $_[0]; @params{@$non_id_properties} = delete @$self{@$non_id_properties}; my $delegate = $ref_class_name->get_or_create(%params); return undef unless $delegate; $_[0]->{ $accessor_name } = $delegate->id; } return $_[0]->{ $accessor_name }; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_rw_class_accessor { my ($self, $class_name, $accessor_name, $column_name, $is_transient, $variable_value, $calc_default) = @_; my $full_accessor_name = $class_name . "::" . $accessor_name; my $accessor = Sub::Name::subname $full_accessor_name => sub { if (@_ > 1) { my $old = $variable_value; $variable_value = $_[1]; my $different = eval { no warnings; $old ne $variable_value }; if ($different or $@ =~ m/has no overloaded magic/) { $_[0]->__signal_change__( $accessor_name, $old, $variable_value ) unless $is_transient; } } elsif (defined $calc_default) { $variable_value = $calc_default->(); } undef $calc_default; return $variable_value; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_ro_class_accessor { my($self, $class_name, $accessor_name, $column_name, $variable_value, $calc_default) = @_; my $full_accessor_name = $class_name . "::" . $accessor_name; my $accessor = Sub::Name::subname $full_accessor_name => sub { if (@_ > 1) { my $new = $_[1]; my $different = eval { no warnings; $variable_value ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $new = defined($new) ? $new : '(undef)'; my $report_variable_value = defined($variable_value) ? $variable_value : '(undef)'; Carp::croak("Cannot change read-only class-wide property $accessor_name for class $class_name from $report_variable_value to $new!"); } } elsif (defined $calc_default) { $variable_value = $calc_default->(); } undef $calc_default; return $variable_value; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_object_set_accessors { my ($self, $class_name, $singular_name, $plural_name, $reverse_as, $r_class_name, $where) = @_; # These are set by the resolver closure below, and kept in scope by the other closures my $rule_template; my $r_class_meta; my @property_names; my @where = ($where ? @$where : ()); my $rule_resolver = sub { my ($obj) = @_; my $loading_r_class_error = ''; if (defined $r_class_name) { eval { $r_class_meta = UR::Object::Type->is_loaded($r_class_name); unless ($r_class_meta or __PACKAGE__->use_module_with_namespace_constraints($r_class_name)) { # Don't die yet. The named class may not have a file associated with it $loading_r_class_error = "Couldn't load class $r_class_name: $@"; $@ = ''; } unless ($r_class_meta) { $r_class_name->class; $r_class_meta = UR::Object::Type->get(class_name => $r_class_name); } }; if ($@) { $loading_r_class_error .= "Couldn't get class object for $r_class_name: $@"; } } if ($r_class_meta and not $reverse_as) { # We have a real class on the other end, and it did not specify know to link back to us. # Try to infer how, otherwise fall back to the same logic we use with "primitives". my @possible_relationships = grep { $_->data_type eq $class_name } grep { defined $_->data_type } $r_class_meta->all_property_metas(); if (@possible_relationships > 1) { Carp::croak "$class_name has an ambiguous definition for property \"$singular_name\"." . " The target class $r_class_name has " . scalar(@possible_relationships) . " relationships which reference back to $class_name." . " Correct by adding \"reverse_as => X\" to ${class_name}'s \"$singular_name\" definition one of the following values: " . join(",",map { '"' . $_->delegation_name . '"' } @possible_relationships) . ".\n"; } elsif (@possible_relationships == 1) { $reverse_as = $possible_relationships[0]->property_name; } elsif (@possible_relationships == 0) { # we now fall through to the logic below and try direct arrayref storage #die "No relationships found between $r_class_name and $class_name. Error in definition for $class_name $singular_name!" } } if ($reverse_as and ! $r_class_meta) { # we've resolved reverse_as, but there's not r_class_meta?! $self->error_message("Can't resolve reverse relationship $class_name -> $plural_name. No class metadata for $r_class_name"); if ($loading_r_class_error) { Carp::croak "While loading $r_class_name: $loading_r_class_error"; } else { Carp::croak "Is class $r_class_name defined anywhere?"; } } if ($reverse_as) { # join to get the data... unless ($r_class_meta) { Carp::croak("No remote class metadata found for class $r_class_name while resolving property '$singular_name' of class $class_name"); } my $property_meta = $r_class_meta->property_meta_for_name($reverse_as); unless ($property_meta) { Carp::croak "Can't resolve reverse relationship $class_name -> $plural_name. Remote class $r_class_name has no property $reverse_as"; } my @get_params; if ($property_meta->via) { # get_property_name_pairs_for_join() only works for properties connected directly. # we still need to use it during initialization, but for more complicated relationships # this should do the right thing push @get_params, $property_meta->property_name . '.id' => $obj->id; push @property_names, 'id'; } else { my @property_links = $property_meta->get_property_name_pairs_for_join; for my $link (@property_links) { my $my_property_name = $link->[1]; push @property_names, $my_property_name; unless ($obj->can($my_property_name)) { Carp::croak "Cannot handle indirect relationship $r_class_name -> $reverse_as. Class $class_name has no property named $my_property_name"; } push @get_params, $link->[0], ($obj->$my_property_name || undef); } } if (my $id_class_by = $property_meta->id_class_by) { push @get_params, $id_class_by, $obj->class; push @property_names, 'class'; } my $tmp_rule = $r_class_name->define_boolexpr(@get_params,@where); if (my $order_by = $property_meta->order_by) { push @get_params, $order_by; } $rule_template = $tmp_rule->template; unless ($rule_template) { Carp::croak "Error generating rule template to handle indirect relationship $class_name $singular_name referencing $r_class_name!"; } return $tmp_rule; } else { # data is stored locally on the hashref #die "No relationships found between $r_class_name and $class_name. Error in definition for $class_name $singular_name!" } }; my @where_values; for (my $i = 1; $i < @where; $i+=2) { if (ref($where[$i]) eq 'HASH' and exists($where[$i]->{'operator'})) { push @where_values, $where[$i]->{'value'}; # the operator is already stored in the template } else { push @where_values, $where[$i]; } } # These will behave specially if the rule does not specify the ID, or all of the ID. my @params_prefix; my $params_prefix_resolved = 0; my $params_prefix_resolver = sub { # handle the case of has-many primitives return unless $r_class_meta; my $r_ids = $r_class_meta->property_meta_for_name($reverse_as)->{id_by}; my $cmeta = UR::Object::Type->get($class_name); my $pmeta = $plural_name ? $cmeta->{has}{$plural_name} : $cmeta->{has}{$singular_name}; if (my $specify_by = $pmeta->{specify_by}) { @params_prefix = ($specify_by); } else { # TODO: should this really be an auto-setting of the specify_by meta property? my @id_property_names = $r_class_name->__meta__->id_property_names; @params_prefix = grep { my $id_property_name = $_; ( (grep { $id_property_name eq $_ } @$r_ids) ? 0 : 1) } @id_property_names; # We only do the special single-value spec when there is one property not specified by the rule. # This is common for a multi-column primary key where all columns reference a parent object, except an index value, etc. @params_prefix = () unless scalar(@params_prefix) == 1; } $params_prefix_resolved = 1; }; if (!$plural_name || $singular_name ne $plural_name) { my $single_accessor = Sub::Name::subname $class_name ."::$singular_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $params_prefix_resolver->() unless $params_prefix_resolved; unshift @_, @params_prefix if @_ == 1; if (@_) { return my $obj = $r_class_name->get($rule->params_list,@_); } else { return my $obj = $r_class_name->get($rule); } } else { return unless $self->{$plural_name}; return unless @_; # Can't compare our list to nothing... if (@_ > 1) { Carp::croak "rule-based selection of single-item accessor not supported. Instead of single value, got @_"; } unless (ref($self->{$plural_name}) eq 'ARRAY') { Carp::croak("${class_name}::$singular_name($_[0]): $plural_name does not contain an arrayref"); } no warnings 'uninitialized'; my @matches = grep { $_ eq $_[0] } @{ $self->{$plural_name} }; return $matches[0] if @matches < 2; return $self->context_return(@matches); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name, code => $single_accessor, }); # return now for reverse_as but not is_many unless ($plural_name) { return; } } my $rule_name = $self->rule_accessor_name_for_is_many_accessor($plural_name); my $rule_accessor = Sub::Name::subname $class_name ."::$rule_name" => sub { my $self = shift; $rule_resolver->($self) unless ($rule_template); unless ($rule_template) { Carp::croak "No indirect rule available for locally-stored 'has-many' relationship"; } if (@_) { my $tmp_rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values); return $r_class_name->define_boolexpr($tmp_rule->params_list, @_); } else { return $rule_template->get_rule_for_values((map { $self->$_ } @property_names),@where_values); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $rule_name, code => $rule_accessor, }); my $list_accessor = Sub::Name::subname $class_name ."::$plural_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); if (@_) { return $UR::Context::current->query($r_class_name, $rule->params_list,@_); } else { return $UR::Context::current->query($r_class_name, $rule); } } else { if (@_) { if (@_ != 1) { Carp::croak "expected a single arrayref when setting a multi-value $class_name $plural_name! Got " . scalar(@_) . " args"; } elsif ( ref($_[0]) ne 'ARRAY' ) { $self->{$plural_name} = [ $_[0] ]; } else { $self->{$plural_name} = [ @{$_[0]} ]; } return @{ $self->{$plural_name} }; } else { return unless $self->{$plural_name}; if (ref($self->{$plural_name}) ne 'ARRAY') { Carp::carp("$class_name with id ".$self->id." does not hold an arrayref in its $plural_name property"); $self->{$plural_name} = [ $self->{$plural_name} ]; } return @{ $self->{$plural_name} }; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $plural_name, code => $list_accessor, }); Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name . '_list', code => $list_accessor, }); my $arrayref_name = $self->arrayref_accessor_name_for_is_many_accessor($plural_name); my $arrayref_accessor = Sub::Name::subname $class_name ."::$arrayref_name" => sub { return [ $list_accessor->(@_) ]; }; Sub::Install::reinstall_sub({ into => $class_name, as => $arrayref_name, code => $arrayref_accessor, }); my $iterator_name = $self->iterator_accessor_name_for_is_many_accessor($plural_name); my $iterator_accessor = Sub::Name::subname $class_name ."::$iterator_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); if (@_) { return $r_class_name->create_iterator($rule->params_list,@_); } else { return UR::Object::Iterator->create_for_filter_rule($rule); } } else { return UR::Value::Iterator->create_for_value_arrayref($self->{$plural_name} || []); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $iterator_name, code => $iterator_accessor, }); my $set_name = $self->set_accessor_name_for_is_many_accessor($plural_name); my $set_accessor = Sub::Name::subname $class_name ."::$set_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names),@where_values) unless (defined $rule); return $r_class_name->define_set($rule->params_list,@_); } else { # this is a bit inside-out, but works for primitives my @members = $self->$plural_name; return UR::Value->define_set(id => \@members); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $set_name, code => $set_accessor, }); my $adder_method_name = $self->adder_name_for_is_many_accessor($plural_name); if ($class_name->can($adder_method_name)) { $adder_method_name = '__' . $adder_method_name; } my $adder_method = Sub::Name::subname $class_name . '::' . $adder_method_name => sub { # TODO: this handles only a single item when making objects: support a list of hashrefs my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $params_prefix_resolver->() unless $params_prefix_resolved; unshift @_, @params_prefix if @_ == 1; $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $r_class_name->create($rule->params_list,@_); } else { if ($r_class_meta) { my $obj; if (@_ == 1 and $_[0]->isa($r_class_name)) { $obj = $_[0]; } else { $obj = $r_class_name->create(@where,@_); unless ($obj) { $self->error_message("Failed to add $singular_name:" . $r_class_name->error_message); return; } } push @{ $self->{$plural_name} ||= [] }, $obj; } else { if (@_ != 1) { Carp::croak "$class_name $adder_method_name expects a single value to add. Got " . scalar(@_) . " args"; } push @{ $self->{$plural_name} ||= [] }, $_[0]; return $_[0]; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $adder_method_name, code => $adder_method, }); my $remover_method_name = $self->remover_name_for_is_many_accessor($plural_name); if ($class_name->can($remover_method_name)) { $remover_method_name = '__' . $remover_method_name; } my $remover_method = Sub::Name::subname $class_name . '::' . $remover_method_name => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { # an id-linked "has-many" $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $params_prefix_resolver->() unless $params_prefix_resolved; my @matches; if (@_ == 1 and ref($_[0])) { # the object to remove was passed-in unless ($rule->evaluate($_[0])) { Carp::croak "Object " . $_[0]->__display_name__ . " is not a member of the $singular_name set!"; } @matches = ($_[0]); } else { # the parameters to find objects to remove were passed-in unshift @_, @params_prefix if @_ == 1; # a single "id" is the remainder of the id of the object @matches = $r_class_name->get($rule->params_list,@_); } my $trans = UR::Context::Transaction->begin; @matches = map { $_->delete or Carp::croak "Error deleting $r_class_name " . $_->id . " for $remover_method_name!: " . $_->error_message; } @matches; $trans->commit; return @matches; } else { # direct storage in an arrayref $self->{$plural_name} ||= []; if ($r_class_meta) { # object my @remove; my @keep; my $rule = $r_class_name->define_boolexpr(@_); for my $value (@{ $self->{$plural_name} }) { if ($rule->evaluate($value)) { push @keep, $value; } else { push @remove, $value; } } if (@remove) { @{ $self->{$plural_name} } = @keep; } return @remove; } else { # value (or non-ur object) if (@_ == 1) { # remove specific value my $removed; my $n = 0; for my $value (@{ $self->{$plural_name} }) { if ($value eq $_[0]) { $removed = splice(@{ $self->{$plural_name} }, $n, 1); Carp::croak("Internal object inconsistency removing value '$value'. Value '$removed' was removed instead!?") unless $removed eq $value; return $removed; } $n++; } Carp::croak("Failed to find item $_[0] in $class_name $plural_name. Object has " . scalar(@{$self->{$plural_name}}) . " values: ".join(', ', @{$self->{$plural_name}})); } elsif (@_ == 0) { # remove all if no params are specified @{ $self->{$plural_name} ||= [] } = (); } else { Carp::croak("$class_name $remover_method_name should be called with zero or one arg, got ".scalar(@_)); } } } }; # check here Sub::Install::reinstall_sub({ into => $class_name, as => $remover_method_name, code => $remover_method, }); } use Data::Dumper; sub initialize_direct_accessors { my $self = shift; my $class_name = $self->{class_name}; my %id_property_names; for my $property_name (@{ $self->{id_by} }) { $id_property_names{$property_name} = 1; next if $property_name eq "id"; } my %dimensions_by_fk; for my $property_name (sort keys %{ $self->{has} }) { my $property_data = $self->{has}{$property_name}; if ($property_data->{is_dimension}) { my $id_by = $property_data->{id_by}; unless ($id_by) { Carp::croak "No id_by specified for dimension $property_name?"; } if (@$id_by != 1) { Carp::croak "The id_by specified for dimension $property_name must list a single property name!"; } my $dimension_class_name = $property_data->{data_type}; $dimensions_by_fk{$id_by->[0]} = $dimension_class_name; my $ref_class_meta = $dimension_class_name->__meta__; my %remote_id_properties = map { $_ => 1 } $ref_class_meta->id_property_names; my @non_id_properties = grep { not $remote_id_properties{$_} } $ref_class_meta->all_property_names; for my $expected_delegate_property_name (@non_id_properties) { unless ($self->{has}{$expected_delegate_property_name}) { $self->{has}{$expected_delegate_property_name} = { $self->_normalize_property_description( $expected_delegate_property_name, { via => $property_name, to => $expected_delegate_property_name, implied_by => $property_name } ) } } } } } for my $pname (sort keys %{ $self->{has} }) { my $property_name = $pname; # mutable my $accessor_name = $pname; my $property_data = $self->{has}{$property_name}; # handle aliases # the underlying property_name and data will change, though the accessor will not my $n = 0; while ($property_data->{via} and $property_data->{via} eq '__self__') { $property_name = $property_data->{to}; $property_data = $self->{has}{$property_name}; unless ($property_data) { Carp::confess("Property $accessor_name is an alias for $property_name, which does not exist!") } if ($n > 100) { Carp::confess("Deep recursion in property aliases behind $accessor_name!"); } } my $column_name = $property_data->{column_name}; my $is_transient = $property_data->{is_transient}; my $where = $property_data->{where}; do { # Handle the case where the software module has an explicit # override for one of the accessors. no strict 'refs'; my $isa = \@{ $class_name . "::ISA" }; my @old_isa = @$isa; @$isa = (); if ($class_name->can($accessor_name)) { #warn "property $class_name $accessor_name exists!"; $accessor_name = "__$accessor_name"; } @$isa = @old_isa; }; unless ($accessor_name) { Carp::croak("No accessor name for property '$property_name' of class $class_name"); } my $accessor_type; my @calculation_fields = (qw/calculate calc_perl calc_sql calculate_from/); if (my $id_by = $property_data->{id_by}) { my $r_class_name = $property_data->{data_type}; #$self->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where); my $id_class_by = $property_data->{id_class_by}; if ($property_data->{access_as} and $property_data->{access_as} eq 'auto') { $self->mk_id_based_flex_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where, $id_class_by); $self->mk_id_based_object_accessor($class_name, $accessor_name . ($property_data->{is_many} ? '_objs' : '_obj'), $id_by, $r_class_name,$where, $id_class_by); } else { $self->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where, $id_class_by); } } elsif ($property_data->{'is_calculated'} and ! $property_data->{'is_mutable'}) {# and $property_data->{'column_name'}) { # For calculated + immutable properties, their calculation function is called # by UR::Context->create_entity(), which then stores the value in the object's # hash. So, the accessor just needs to pull the data like a regular r/o accessor #$self->mk_ro_accessor($class_name, $accessor_name, $property_data->{'column_name'}); $self->mk_calculation_accessor( $class_name, $accessor_name, $property_data->{'calculate'}, $property_data->{calculate_from}, $property_data->{calculate_params}, 1, # the value should be cached $property_data->{'column_name'}, ); } elsif (my $via = $property_data->{via}) { my $to = $property_data->{to} || $property_data->{property_name}; if ($via eq '__self__') { Carp::croak "aliases should be caught above!"; } if ($property_data->{is_mutable}) { my $singular_name; if ($property_data->{'is_many'}) { require Lingua::EN::Inflect; $singular_name = Lingua::EN::Inflect::PL_V($accessor_name); } $self->mk_indirect_rw_accessor($class_name,$accessor_name,$via,$to,$where,$property_data->{'is_many'} && $singular_name, $property_name); } else { $self->mk_indirect_ro_accessor($class_name,$accessor_name,$via,$to,$where); } } elsif (my $calculate = $property_data->{calculate}) { $self->mk_calculation_accessor( $class_name, $accessor_name, $property_data->{calculate}, $property_data->{calculate_from}, $property_data->{calculate_params}, $property_data->{is_constant}, $property_data->{column_name}, ); } elsif (my $calculate_sql = $property_data->{'calculate_sql'}) { # The data gets filled in by the object loader behind the scenes. # To the user, it's a read-only property $self->mk_ro_accessor($class_name, $accessor_name, $calculate_sql); } elsif ($property_data->{is_many} or $property_data->{reverse_as}){ my $reverse_as = $property_data->{reverse_as}; my $r_class_name = $property_data->{data_type}; my $singular_name; my $plural_name; if ($property_data->{is_many}) { $plural_name = $accessor_name; $singular_name = $self->singular_accessor_name_for_is_many_accessor($accessor_name); } else { $singular_name = $accessor_name; } $self->mk_object_set_accessors($class_name, $singular_name, $plural_name, $reverse_as, $r_class_name, $where); } elsif ($property_data->{'is_classwide'}) { my($value, $column_name, $is_transient, $calc_default) = @$property_data{'default_value','column_name','is_transient', 'calculated_default'}; if ($property_data->{'is_constant'}) { $self->mk_ro_class_accessor($class_name,$accessor_name,$column_name,$value, $calc_default); } else { $self->mk_rw_class_accessor($class_name,$accessor_name,$column_name,$is_transient,$value, $calc_default); } } else { # Just use key/value pairs in the hash for normal # table stuff, and also non-database stuff. #if ($column_name) { # push @$props, $property_name; # push @$cols, $column_name; #} my $maker; if ($id_property_names{$property_name} or not $property_data->{is_mutable}) { $maker = 'mk_ro_accessor'; } else { $maker = 'mk_rw_accessor'; } $self->$maker($class_name, $accessor_name, $column_name, $property_name,$is_transient); } } # right now we just stomp on the default accessors constructed above where they are: # 1. the fk behind a dimensional relationships # 2. the indirect properties created for the dimensional relationship for my $dimension_id (keys %dimensions_by_fk) { my $dimension_class_name = $dimensions_by_fk{$dimension_id}; my $ref_class_meta = $dimension_class_name->__meta__; my %remote_id_properties = map { $_ => 1 } $ref_class_meta->id_property_names; my @non_id_properties = grep { not $remote_id_properties{$_} } $ref_class_meta->all_property_names; for my $added_property_name (@non_id_properties) { $self->mk_dimension_delegate_accessors($dimension_id,$dimension_class_name, \@non_id_properties, $added_property_name); } $self->mk_dimension_identifying_accessor($dimension_id,$dimension_class_name, \@non_id_properties); } return 1; } 1; =pod =head1 NAME UR::Object::Type::AccessorWriter - Helper module for UR::Object::Type responsible for creating accessors for properties =head1 DESCRIPTION Subroutines within this module actually live in the UR::Object::Type namespace; this module is just a convienent place to collect them. The class initializer uses these subroutines when it's time to create accessor methods for a newly defined class. Each accessor is implemented by a closure that is then assigned a name by Sub::Name and inserted into the defined class's namespace by Sub::Install. =head1 METHODS =over 4 =item initialize_direct_accessors $classobj->initialize_direct_accessors(); This is the entry point into the accessor writing system. It inspects each item in the 'has' key of the class object's hashref, and creates methods for each property. =item mk_rw_accessor $classobj->mk_rw_accessor($class_name, $accessor_name, $column_name, $property_name, $is_transient); Creates a mutable accessor named $accessor_name which stores its value in the $property_name key of the object's hashref. =item mk_ro_accessor $classobj->mk_ro_accessor($class_name, $accessor_name, $column_name, $property_name); Creates a read-only accessor named $accessor_name which retrieves its value in the $property_name key of the object's hashref. If the method is used as a mutator by passing in a value to the method, it will throw an exception with Carp::croak. =item mk_id_based_object_accessor $classobj->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name, $where); Creates an object accessor named $accessor_name. It returns objects of type $r_class_name, id-ed by the parameters named in the $id_by arrayref. $where is an optional listref of additional filters to apply when retrieving objects. The behavior of the created accessor depends on the number of parameters passed to it. For 0 params, it retrieves the object pointed to by $r_class_name and $id_by. For 1 param, it looks up the ID param values of the passed-in object-parameter, and reassigns value stored in the $id_by properties of the acted-upon object, effectively acting as a mutator. For more than 1 param, the additional parameters are taken as properties/values to filter the returned objects on =item mk_indirect_ro_accessor $classobj->mk_indirect_ro_accessor($class_name, $accessor_name, $via, $to, $where); Creates a read-only via accessor named $accessor_name. Its value is obtained by calling the object accessor named $via, and then calling the method $to on that object. The optional $where listref is used as additional filters when calling $via. =item mk_indirect_rw_accessor $classobj->mk_indirect_rw_accessor($class_name, $accessor_name, $via, $to, $where, $singular_name); Creates a via accessor named $accessor_name that is able to change the property it points to with $to when called as a mutator. If the $to property on the remote object is an ID property of its class, it deletes the refered-to object and creates a new one with the appropriate properties. Otherwise, it updates the $to property on the refered-to object. =item mk_calculation_accessor $classobj->mk_calculation_accessor($class_name, $accessor_name, $calculation_src, $calculate_from, $params, $is_constant, $column_name); Creates a calculated accessor called $accessor_name. If the $is_constant flag is true, then the accessor runs the calculation once, caches the result, and returns that result for subseqent calls to the accessor. $calculation_src can be one of: coderef, string containing Perl code, or the name of a module under UR::Object::Type::AccessorWriter which has a method called C. If $calculation_src is empty, then $accessor_name must be the name of an already-existing subroutine in the class's namespace. =item mk_dimension_delegate_accessors =item mk_dimension_identifying_accessor These create accessors for dealing with dimension tables in OLAP-type schemas. They need more documentation. =item mk_rw_class_accessor $classobj->mk_rw_class_accessor($class_name, $accessor_name, $column_name, $is_transient, $variable_value); Creates a read-write accessor called $accessor_name which stores its value in a scalar captured by the accessor's closure. Since the closure is inserted into the class's namespace, all instances of the class share the same closure (and therefore the same scalar), and the property effectively acts as a class-wide property. =item mk_ro_class_accessor $classobj->mk_ro_class_accessor($class_name, $accessor_name, $column_name, $variable_value); Creates a read-only accessor called $accessor_name which retrieves its value from a scalar captured by the accessor's closure. The value is initialized to $variable_value. If called as a mutator, it throws an exception through Carp::croak =back =head1 SEE ALSO UR::Object::Type::AccessorWriter, UR::Object::Type =cut Product.pm100664023532023421 76312544604516 21763 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/AccessorWriter package UR::Object::Type::AccessorWriter::Product; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; sub calculate { my $self = shift; my $object = shift; my $properties = shift; my $total = 1; for my $property (@$properties) { $total *= $object->$property } return $total; }; 1; =pod =head1 NAME UR::Object::Type::AccessorWriter::Product - Implements a calculation accessor which multiplies the values of its properties =cut Sum.pm100664023532023421 73712544604516 21110 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/AccessorWriter package UR::Object::Type::AccessorWriter::Sum; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; sub calculate { my $self = shift; my $object = shift; my $properties = shift; my $sum = 0; for my $property (@$properties) { $sum += $object->$property } return $sum; }; 1; =pod =head1 NAME UR::Object::Type::AccessorWriter::Sum - Implements a calculation accessor which sums the values of its properties =cut Initializer.pm100664023532023421 21656712544604516 17762 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type# This line forces correct deployment by some tools. package UR::Object::Type::Initializer; package UR::Object::Type; use strict; use warnings; require UR; BEGIN { # Perl 5.10 did not require mro in order to call get_mro but it looks # like that was "fixed" in newer version. if ($^V ge v5.9.5) { eval "require mro"; } }; our $VERSION = "0.44"; # UR $VERSION; use Carp (); use Sub::Name (); use Sub::Install (); # keys are class property names (like er_role, is_final, etc) and values are # the default value to use if it's not specified in the class definition # # For most classes, this kind of thing is handled by the default_value attribute on # a class' property. For bootstrapping reasons, the default values for the # properties of UR::Object::Type' class need to be listed here as well. If # any of these change, or new default valued items are added, be sure to also # update the class definition for UR::Object::Type (which really lives in UR.pm # for the moment) %UR::Object::Type::defaults = ( er_role => 'entity', is_final => 0, is_singleton => 0, is_transactional => 1, is_mutable => 1, is_many => 0, is_abstract => 0, subclassify_by_version => 0, ); # All those same comments also apply to UR::Object::Property's properties %UR::Object::Property::defaults = ( is_optional => 0, is_transient => 0, is_constant => 0, is_volatile => 0, is_classwide => 0, is_delegated => 0, is_calculated => 0, is_mutable => undef, is_transactional => 1, is_many => 0, is_numeric => 0, is_specified_in_module_header => 0, is_deprecated => 0, position_in_module_header => -1, doc_position => -1, is_undocumented => 0, ); @UR::Object::Type::meta_id_ref_shared_properties = ( qw/ is_optional is_transient is_constant is_volatile is_classwide is_transactional is_abstract is_concrete is_final is_many is_deprecated is_undocumented / ); %UR::Object::Type::converse = ( required => 'optional', abstract => 'concrete', one => 'many', ); # These classes are used to define an object class. # As such, they get special handling to bootstrap the system. our %meta_classes = map { $_ => 1 } qw/ UR::Object UR::Object::Type UR::Object::Property /; our $bootstrapping = 1; our @partially_defined_classes; # When copying the object hash to create its db_committed, these keys should be removed because # they contain things like coderefs our @keys_to_delete_from_db_committed = qw( id db_committed _id_property_sorter get_composite_id_resolver get_composite_id_decomposer ); # Stages of Class Initialization # # define() is called to indicate the class structure (create() may also be called by the db sync command to make new classes) # # the parameters to define()/create() are normalized by _normalize_class_description() # # a basic functional class meta object is created by _define_minimal_class_from_normalized_class_description() # # accessors are created # # if we're still bootstrapping: # # the class is stashed in an array so the post-bootstrapping stages can be done in bulk # # we exit define() # # if we're done bootstrapping: # # _inform_all_parent_classes_of_newly_loaded_subclass() sets up an internal map of known subclasses of each base class # # _complete_class_meta_object_definitions() decomposes the definition into normalized objects # sub __define__ { my $class = shift; my $desc = $class->_normalize_class_description(@_); my $class_name = $desc->{class_name} ||= (caller(0))[0]; $desc->{class_name} = $class_name; my $self; my %params = $class->_construction_params_for_desc($desc); my $meta_class_name; if (%params) { $self = __PACKAGE__->__define__(%params); return unless $self; $meta_class_name = $params{class_name}; } else { $meta_class_name = __PACKAGE__; } $self = $UR::Context::all_objects_loaded->{$meta_class_name}{$class_name}; if ($self) { #$DB::single = 1; #Carp::cluck("Re-defining class $class_name? Found $meta_class_name with id '$class_name'"); return $self; } $self = $class->_make_minimal_class_from_normalized_class_description($desc); Carp::confess("Failed to define class $class_name!") unless $self; # we do this for define() but not create() my %db_committed = %$self; delete @db_committed{@keys_to_delete_from_db_committed}; $self->{'db_committed'} = \%db_committed; $self->_initialize_accessors_and_inheritance or Carp::confess("Error initializing accessors for $class_name!"); if ($bootstrapping) { push @partially_defined_classes, $self; } else { unless ($self->_inform_all_parent_classes_of_newly_loaded_subclass()) { Carp::confess( "Failed to link to parent classes to complete definition of class $class_name!" . $class->error_message ); } unless ($self->_complete_class_meta_object_definitions()) { #$DB::single = 1; $self->_complete_class_meta_object_definitions(); Carp::confess( "Failed to complete definition of class $class_name!" . $class->error_message ); } } return $self; } sub create { # this is typically only used by code which intendes to autogenerate source code # it will lead to the writing of a Perl module upon commit. my $class = shift; my $desc = $class->_normalize_class_description(@_); my $class_name = $desc->{class_name} ||= (caller(0))[0]; my $meta_class_name = $desc->{meta_class_name}; no strict 'refs'; unless ( $meta_class_name eq __PACKAGE__ or # in newer Perl interpreters the ->isa() call can return true # even if @ISA has been emptied (OS X) ??? (scalar(@{$meta_class_name . '::ISA'}) and $meta_class_name->isa(__PACKAGE__)) ) { if (__PACKAGE__->get(class_name => $meta_class_name)) { warn "class $meta_class_name already exists when creating class meta for $class_name?!"; } else { __PACKAGE__->create( __PACKAGE__->_construction_params_for_desc($desc) ); } } my $self = $class->_make_minimal_class_from_normalized_class_description($desc); Carp::confess("Failed to define class $class_name!") unless $self; $self->_initialize_accessors_and_inheritance or Carp::confess("Failed to define class $class_name!"); $self->_inform_all_parent_classes_of_newly_loaded_subclass() or Carp::confess( "Failed to link to parent classes to complete definition of class $class_name!" . $class->error_message ); $self->generated(0); $self->__signal_change__("create"); return $self; } sub _preprocess_subclass_description { # allow a class to modify the description of any subclass before it instantiates # this filtering allows a base class to specify policy, add meta properties, etc. my ($self,$prev_desc) = @_; my $current_desc = $prev_desc; if (my $preprocessor = $self->subclass_description_preprocessor) { # the preprocessor must me a method name in the class being adjusted no strict 'refs'; unless ($self->class_name->can($preprocessor)) { die "Class " . $self->class_name . " specifies a pre-processor for subclass descriptions " . $preprocessor . " which is not defined in the " . $self->class_name . " package!"; } $current_desc = $self->class_name->$preprocessor($current_desc); $current_desc = $self->_normalize_class_description_impl(%$current_desc); } # only call it on the direct parent classes, let recursion walk the tree my @parent_class_names = grep { $_->can('__meta__') } $self->parent_class_names(); for my $parent_class_name (@parent_class_names) { my $parent_class = $parent_class_name->__meta__; $current_desc = $parent_class->_preprocess_subclass_description($current_desc); } return $current_desc; } sub _construction_params_for_desc { my $class = shift; my $desc = shift; my $class_name = $desc->{class_name}; my $meta_class_name = $desc->{meta_class_name}; my @extended_metadata; if ($desc->{type_has}) { @extended_metadata = ( has => [ @{ $desc->{type_has} } ] ); } if ( $meta_class_name eq __PACKAGE__ ) { if (@extended_metadata) { die "Cannot extend class metadata of $class_name because it is a class involved in UR bootstrapping."; } return(); } else { if ($bootstrapping) { return ( class_name => $meta_class_name, is => __PACKAGE__, @extended_metadata, ); } else { my $parent_classes = $desc->{is}; my @meta_parent_classes = map { $_ . '::Type' } @$parent_classes; for (@$parent_classes) { __PACKAGE__->use_module_with_namespace_constraints($_); eval {$_->class}; if ($@) { die "Error with parent class $_ when defining $class_name! $@"; } } return ( class_name => $meta_class_name, is => \@meta_parent_classes, @extended_metadata, ); } } } sub initialize_bootstrap_classes { # This is called once at the end of compiling the UR module set to handle # classes which did incomplete initialization while bootstrapping. # Until bootstrapping occurs is done, my $class = shift; for my $class_meta (@partially_defined_classes) { unless ($class_meta->_inform_all_parent_classes_of_newly_loaded_subclass) { my $class_name = $class_meta->{class_name}; Carp::confess ( "Failed to complete inheritance linkage definition of class $class_name!" . $class_meta->error_message ); } } while (my $class_meta = shift @partially_defined_classes) { unless ($class_meta->_complete_class_meta_object_definitions()) { my $class_name = $class_meta->{class_name}; Carp::confess( "Failed to complete definition of class $class_name!" . $class_meta->error_message ); } } $bootstrapping = 0; # It should be safe to set up callbacks now. register_callback() instead # of create() so a subsequent rollback won't remove the observer. UR::Observer->register_callback( subject_class_name => 'UR::Object::Property', subject_id => '', aspect => '', priority => 1, note => '', once => 0, callback => \&UR::Object::Type::_property_change_callback, ); } sub _normalize_class_description { my $class = shift; my $desc = $class->_normalize_class_description_impl(@_); unless ($bootstrapping) { for my $parent_class_name (@{ $desc->{is} }) { my $parent_class = $parent_class_name->__meta__; $desc = $parent_class->_preprocess_subclass_description($desc); } } # we previously handled property meta extensions when normalizing the property # now we merely save unrecognized things # this is now done afterward so that parent classes can preprocess their subclasses descriptions before extending # normalize the data behind the property descriptions my @property_names = keys %{$desc->{has}}; for my $property_name (@property_names) { Carp::croak("Invalid property name in class ".$desc->{class_name}.": '$property_name'") unless UR::Util::is_valid_property_name($property_name); my $pdesc = $desc->{has}->{$property_name}; my $unknown_ma = delete $pdesc->{unrecognized_meta_attributes}; next unless $unknown_ma; for my $name (keys %$unknown_ma) { if (exists $desc->{attributes_have}->{$name}) { $pdesc->{$name} = delete $unknown_ma->{$name}; } } if (%$unknown_ma) { my $class_name = $desc->{class_name}; my @unknown_ma = sort keys %$unknown_ma; Carp::confess("unknown meta-attributes present for $class_name $property_name: @unknown_ma\n"); } } return $desc; } sub _normalize_class_description_impl { my $class = shift; my %old_class = @_; if (exists $old_class{extra}) { %old_class = (%{delete $old_class{extra}}, %old_class); } my $class_name = delete $old_class{class_name}; my %new_class = ( class_name => $class_name, is_singleton => $UR::Object::Type::defaults{'is_singleton'}, is_final => $UR::Object::Type::defaults{'is_final'}, is_abstract => $UR::Object::Type::defaults{'is_abstract'}, ); for my $mapping ( [ class_name => qw//], [ type_name => qw/english_name/], [ is => qw/inheritance extends isa is_a/], [ is_abstract => qw/abstract/], [ is_final => qw/final/], [ is_singleton => qw//], [ is_transactional => qw//], [ id_by => qw/id_properties/], [ has => qw/properties/], [ type_has => qw//], [ attributes_have => qw//], [ er_role => qw/er_type/], [ doc => qw/description/], [ relationships => qw//], [ constraints => qw/unique_constraints/], [ namespace => qw//], [ schema_name => qw//], [ data_source_id => qw/data_source instance/], [ table_name => qw/sql dsmap/], [ select_hint => qw/query_hint/], [ join_hint => qw//], [ subclassify_by => qw/sub_classification_property_name/], [ sub_classification_meta_class_name => qw//], [ sub_classification_method_name => qw//], [ first_sub_classification_method_name => qw//], [ composite_id_separator => qw//], [ generate => qw//], [ generated => qw//], [ subclass_description_preprocessor => qw//], [ id_generator => qw/id_sequence_generator_name/], [ subclassify_by_version => qw//], [ meta_class_name => qw//], [ valid_signals => qw//], ) { my ($primary_field_name, @alternate_field_names) = @$mapping; my @all_fields = ($primary_field_name, @alternate_field_names); my @values = grep { defined($_) } delete @old_class{@all_fields}; if (@values > 1) { Carp::confess( "Multiple values in class definition for $class_name for field " . join("/", @all_fields) ); } elsif (@values == 1) { $new_class{$primary_field_name} = $values[0]; } } if (my $pp = $new_class{subclass_description_preprocessor}) { if (!ref($pp)) { unless ($pp =~ /::/) { # a method name, not fully qualified $new_class{subclass_description_preprocessor} = $new_class{class_name} . '::' . $new_class{subclass_description_preprocessor}; } else { $new_class{subclass_description_preprocessor} = $pp; } } elsif (ref($pp) ne 'CODE') { die "unexpected " . ref($pp) . " reference for subclass_description_preprocessor for $class_name!"; } } unless ($new_class{er_role}) { $new_class{er_role} = $UR::Object::Type::defaults{'er_role'}; } my @crap = qw/source/; delete @old_class{@crap}; if ($class_name =~ /^(.*?)::/) { $new_class{namespace} = $1; } else { $new_class{namespace} = $new_class{class_name}; } if (not exists $new_class{is_transactional} and not $meta_classes{$class_name} ) { $new_class{is_transactional} = $UR::Object::Type::defaults{'is_transactional'}; } unless ($new_class{is}) { no warnings; no strict 'refs'; if (my @isa = @{ $class_name . "::ISA" }) { $new_class{is} = \@isa; } } unless ($new_class{is}) { if ($new_class{table_name}) { $new_class{is} = ['UR::Entity'] } else { $new_class{is} = ['UR::Object'] } } unless ($new_class{'doc'}) { $new_class{'doc'} = undef; } if ($new_class{'valid_signals'}) { if (!ref($new_class{'valid_signals'})) { # If it's a plain string, wrap it into an arrayref $new_class{'valid_signals'} = [ $new_class{'valid_signals'} ]; } elsif (ref($new_class{'valid_signals'}) ne 'ARRAY') { Carp::confess("The 'valid_signals' metadata for class $class_name must be an arrayref"); } } else { $new_class{'valid_signals'} = []; } for my $field (qw/is id_by has relationships constraints/) { next unless exists $new_class{$field}; my $reftype = ref($new_class{$field}); if (! $reftype) { # It's a plain string, wrap it in an arrayref $new_class{$field} = [ $new_class{$field} ]; } elsif ($reftype eq 'HASH') { # Later code expects it to be a listref - convert it my @params_as_list; foreach my $attr_name ( keys (%{$new_class{$field}}) ) { push @params_as_list, $attr_name; push @params_as_list, $new_class{$field}->{$attr_name}; } $new_class{$field} = \@params_as_list; } elsif ($reftype ne 'ARRAY') { die "Class $class_name cannot initialize because its $field section is not a string, arrayref or hashref"; } } # These may have been found and moved over. Restore. $old_class{has} = delete $new_class{has}; $old_class{attributes_have} = delete $new_class{attributes_have}; # Install structures to track fully formatted property data. my $instance_properties = $new_class{has} = {}; my $meta_properties = $new_class{attributes_have} = {}; # The id might be a single value, or not specified at all. my $id_properties; if (not exists $new_class{id_by}) { if ($new_class{is}) { $id_properties = $new_class{id_by} = []; } else { $id_properties = $new_class{id_by} = [ id => { is_optional => 0 } ]; } } elsif ( (not ref($new_class{id_by})) or (ref($new_class{id_by}) ne 'ARRAY') ) { $id_properties = $new_class{id_by} = [ $new_class{id_by} ]; } else { $id_properties = $new_class{id_by}; } # Transform the id properties into a list of raw ids, # and move the property definitions into "id_implied" # where present so they can be processed below. my $property_rank = 0; do { my @replacement; my $pos = 0; for (my $n = 0; $n < @$id_properties; $n++) { my $name = $id_properties->[$n]; my $data = $id_properties->[$n+1]; if (ref($data)) { $old_class{id_implied}->{$name} ||= $data; if (my $obj_ids = $data->{id_by}) { push @replacement, (ref($obj_ids) ? @$obj_ids : ($obj_ids)); } else { push @replacement, $name; } $n++; } else { $old_class{id_implied}->{$name} ||= {}; push @replacement, $name; } $old_class{id_implied}->{$name}->{'position_in_module_header'} = $pos++; } @$id_properties = @replacement; }; if (@$id_properties > 1 and grep {$_ eq 'id'} @$id_properties) { Carp::croak("Cannot initialize class $class_name: " . "Cannot have an ID property named 'id' when the class has multiple ID properties (" . join(', ', map { "'$_'" } @$id_properties) . ")"); } # Flatten and format the property list(s) in the class description. # NOTE: we normalize the details at the end of normalizing the class description. my @keys = _class_definition_property_keys_in_processing_order(\%old_class); foreach my $key ( @keys ) { # parse the key to see if we're looking at instance or meta attributes, # and take the extra words as additional attribute meta-data. my @added_property_meta; my $properties; if ($key =~ /has/) { @added_property_meta = grep { $_ ne 'has' } split(/[_-]/,$key); $properties = $instance_properties; } elsif ($key =~ /attributes_have/) { @added_property_meta = grep { $_ ne 'attributes' and $_ ne 'have' } split(/[_-]/,$key); $properties = $meta_properties; } elsif ($key eq 'id_implied') { # these are additions to the regular "has" list from complex identity properties $properties = $instance_properties; } else { die "Odd key $key?"; } @added_property_meta = map { 'is_' . $_ => 1 } @added_property_meta; # the property data can be a string, array, or hash as they come in # convert string, hash and () into an array my $property_data = delete $old_class{$key}; my @tmp; if (!ref($property_data)) { if (defined($property_data)) { @tmp = split(/\s+/, $property_data); } else { @tmp = (); } } elsif (ref($property_data) eq 'HASH') { @tmp = map { ($_ => $property_data->{$_}) } sort keys %$property_data; } elsif (ref($property_data) eq 'ARRAY') { @tmp = @$property_data; } else { die "Unrecognized data $property_data appearing as property list!"; } # process the array of property specs my $pos = 0; while (my $name = shift @tmp) { my $params; if (ref($tmp[0])) { $params = shift @tmp; unless (ref($params) eq 'HASH') { my $seen_type = ref($params); Carp::confess("class $class_name property $name has a $seen_type reference instead of a hashref describing its meta-attributes!"); } %$params = (@added_property_meta, %$params) if @added_property_meta; } else { $params = { @added_property_meta }; } unless (exists $params->{'position_in_module_header'}) { $params->{'position_in_module_header'} = $pos++; } unless (exists $params->{is_specified_in_module_header}) { $params->{is_specified_in_module_header} = $class_name . '::' . $key; } # Indirect properties can mention the same property name more than once. To # avoid stomping over existing property data with this other property data, # merge the new info into the existing hash. Otherwise, the new property name # gets an empty hash of info if ($properties->{$name}) { # this property already exists, but is also implied by some other property which added it to the end of the listed # extend the existing definition foreach my $key ( keys %$params ) { next if ($key eq 'is_specified_in_module_header' || $key eq 'position_in_module_header'); # once a property gets set to is_optional => 0, it stays there, even if it's later set to 1 next if ($key eq 'is_optional' and exists($properties->{$name}->{'is_optional'}) and defined($properties->{$name}->{'is_optional'}) and $properties->{$name}->{'is_optional'} == 0); $properties->{$name}->{$key} = $params->{$key}; } $params = $properties->{$name}; } else { $properties->{$name} = $params; } # a single calculate_from can be a simple string, convert to a listref if (my $calculate_from = $params->{'calculate_from'}) { $params->{'calculate_from'} = [ $calculate_from ] unless (ref($calculate_from) eq 'ARRAY'); } if (my $id_by = $params->{id_by}) { $id_by = [ $id_by ] unless ref($id_by) eq 'ARRAY'; my @id_by_names; while (@$id_by) { my $id_name = shift @$id_by; my $params2; if (ref($id_by->[0])) { $params2 = shift @$id_by; } else { $params2 = {}; } for my $p (@UR::Object::Type::meta_id_ref_shared_properties) { if (exists $params->{$p}) { $params2->{$p} = $params->{$p}; } } $params2->{implied_by} = $name; $params2->{is_specified_in_module_header} = 0; push @id_by_names, $id_name; push @tmp, $id_name, $params2; } $params->{id_by} = \@id_by_names; } if (my $id_class_by = $params->{'id_class_by'}) { if (ref $id_class_by) { Carp::croak("Cannot initialize class $class_name: " . "Property $name has an 'id_class_by' that is not a plain string"); } push @tmp, $id_class_by, { implied_by => $name, is_specified_in_module_header => 0 }; } } # next property in group # id-by properties' metadata can influence the id-ed-by property metadata for my $pdata (values %$properties) { next unless $pdata->{id_by}; for my $id_property (@{ $pdata->{id_by} }) { my $id_pdata = $properties->{$id_property}; for my $p (@UR::Object::Type::meta_id_ref_shared_properties) { if (exists $id_pdata->{$p} xor exists $pdata->{$p}) { # if one or the other specifies a value, copy it to the one that's missing $id_pdata->{$p} = $pdata->{$p} = $id_pdata->{$p} || $pdata->{$p}; } elsif (!exists $id_pdata->{$p} and !exists $pdata->{$p} and exists $UR::Object::Property::defaults{$p}) { # if neither has a value, use the default for both $id_pdata->{$p} = $pdata->{$p} = $UR::Object::Property::defaults{$p}; } } } } } # next group of properties # NOT ENABLED YET if (0) { # done processing direct properties of this process # extend %$instance_properties with properties of the parent classes my @parent_class_names = @{ $new_class{is} }; for my $parent_class_name (@parent_class_names) { my $parent_class_meta = $parent_class_name->__meta__; die "no meta for $parent_class_name while initializing $class_name?" unless $parent_class_meta; my $parent_normalized_properties = $parent_class_meta->{has}; for my $parent_property_name (keys %$parent_normalized_properties) { my $parent_property_data = $parent_normalized_properties->{$parent_property_name}; my $inherited_copy = $instance_properties->{$parent_property_name}; unless ($inherited_copy) { $inherited_copy = UR::Util::deep_copy($parent_property_data); } $inherited_copy->{class_name} = $class_name; my $override = $inherited_copy->{overrides_class_names} ||= []; push @$override, $parent_property_data->{class_name}; } } } if (($new_class{data_source_id} and not ref($new_class{data_source_id})) and not $new_class{schema_name}) { my $s = $new_class{data_source_id}; $s =~ s/^.*::DataSource:://; $new_class{schema_name} = $s; } if (%old_class) { # this should have all been deleted above # we actually process it later, since these may be related to parent classes extending # the class definition $new_class{extra} = \%old_class; }; # ensure parent classes are loaded unless ($bootstrapping) { my @base_classes = map { ref($_) ? @$_ : $_ } $new_class{is}; for my $parent_class_name (@base_classes) { # ensure the parent classes are fully processed no warnings; unless ($parent_class_name->can("__meta__")) { __PACKAGE__->use_module_with_namespace_constraints($parent_class_name); Carp::croak("Class $class_name cannot initialize because of errors using parent class $parent_class_name: $@") if $@; } unless ($parent_class_name->can("__meta__")) { if ($ENV{'HARNESS_ACTIVE'}) { Carp::confess("Class $class_name cannot initialize because of errors using parent class $parent_class_name. Failed to find static method '__meta__' on $parent_class_name. Does class $parent_class_name exist, and is it loaded?\n The entire list of base classes was ".join(', ', @base_classes)); } Carp::croak("Class $class_name cannot initialize because of errors using parent class $parent_class_name. Failed to find static method '__meta__' on $parent_class_name. Does class $parent_class_name exist, and is it loaded?"); } my $parent_class = $parent_class_name->__meta__; unless ($parent_class) { Carp::carp("No class metadata object for $parent_class_name"); next; } # the the parent classes indicate version, if needed if ($parent_class->{'subclassify_by_version'} and not $parent_class_name =~ /::Ghost/) { unless ($class_name =~ /^${parent_class_name}::V\d+/) { my $ns = $parent_class_name; $ns =~ s/::.*//; my $version; if ($ns and $ns->can("component_version")) { $version = $ns->component_version($class); } unless ($version) { $version = '1'; } $parent_class_name = $parent_class_name . '::V' . $version; eval "use $parent_class_name"; Carp::confess("Error using versioned module $parent_class_name!:\n$@") if $@; redo; } } } $new_class{is} = \@base_classes; } # normalize the data behind the property descriptions my @property_names = keys %$instance_properties; for my $property_name (@property_names) { my %old_property = %{ $instance_properties->{$property_name} }; my %new_property = $class->_normalize_property_description1($property_name, \%old_property, \%new_class); %new_property = $class->_normalize_property_description2(\%new_property, \%new_class); $instance_properties->{$property_name} = \%new_property; } # allow parent classes to adjust the description in systematic ways my $desc = \%new_class; my @additional_property_meta_attributes; unless ($bootstrapping) { for my $parent_class_name (@{ $new_class{is} }) { my $parent_class = $parent_class_name->__meta__; if (my $parent_meta_properties = $parent_class->{attributes_have}) { push @additional_property_meta_attributes, %$parent_meta_properties; } } } # Find 'via' properties where the to is '-filter' and rewrite them to # copy some attributes from the source property # This feels like a hack, but it makes other parts of the system easier by # not having to deal with -filter foreach my $property_name ( @property_names ) { my $property_data = $instance_properties->{$property_name}; if ($property_data->{'to'} && $property_data->{'to'} eq '-filter') { my $via = $property_data->{'via'}; my $via_property_data = $instance_properties->{$via}; unless ($via_property_data) { Carp::croak "Cannot initialize class $class_name: Property '$property_name' filters '$via', but there is no property '$via'."; } $property_data->{'data_type'} = $via_property_data->{'data_type'}; $property_data->{'reverse_as'} = $via_property_data->{'reverse_as'}; if ($via_property_data->{'where'}) { unshift @{$property_data->{'where'}}, @{$via_property_data->{'where'}}; } } } # Catch a mistake in the class definition where a property is 'via' # something, and its 'to' is the same as the via's reverse_as. This # ends up being a circular definition and generates junk SQL foreach my $property_name ( @property_names ) { my $property_data = $instance_properties->{$property_name}; my $via = $property_data->{'via'}; my $to = $property_data->{'to'}; if (defined($via) and defined($to)) { my $via_property_data = $instance_properties->{$via}; next unless ($via_property_data and $via_property_data->{'reverse_as'}); if ($via_property_data->{'reverse_as'} eq $to) { Carp::croak("Cannot initialize class $class_name: Property '$property_name' defines " . "an incompatible relationship. Its 'to' is the same as reverse_as for property '$via'"); } } } unless ($bootstrapping) { # cascade extra meta attributes from the parent downward for my $parent_class_name (@{ $new_class{is} }) { my $parent_class = $parent_class_name->__meta__; if (my $parent_meta_properties = $parent_class->{attributes_have}) { #push @additional_property_meta_attributes, %$parent_meta_properties; } } %$meta_properties = (%$meta_properties, @additional_property_meta_attributes); # Inheriting from an abstract class that subclasses with a subclassify_by means that # this class' property named by that subclassify_by is actually a constant equal to this # class' class name PARENT_CLASS: foreach my $parent_class_name ( @{ $new_class{'is'} }) { my $parent_class_meta = $parent_class_name->__meta__(); foreach my $ancestor_class_meta ( $parent_class_meta->all_class_metas ) { if (my $subclassify_by = $ancestor_class_meta->subclassify_by) { if (not $instance_properties->{$subclassify_by}) { my %old_property = ( property_name => $subclassify_by, default_value => $class_name, is_constant => 1, is_classwide => 1, is_specified_in_module_header => 0, column_name => '', implied_by => $parent_class_meta->class_name . '::subclassify_by', ); my %new_property = $class->_normalize_property_description1($subclassify_by, \%old_property, \%new_class); my %new_property2 = $class->_normalize_property_description2(\%new_property, \%new_class); $instance_properties->{$subclassify_by} = \%new_property2; last PARENT_CLASS; } } } } } my $meta_class_name = __PACKAGE__->_resolve_meta_class_name_for_class_name($class_name); $desc->{meta_class_name} ||= $meta_class_name; return $desc; } sub _class_definition_property_keys_in_processing_order { my $class_hashref = shift; my @order; # we want to hit 'id_implied' first to preserve position_ and is_specified_ keys push(@order, 'id_implied') if exists $class_hashref->{id_implied}; # 'has' next so is_optional can get set to 0 in case the same property also appears in has_optional push(@order, 'has') if exists $class_hashref->{has}; # everything else push @order, grep { /has_|attributes_have/ } keys %$class_hashref; return @order; } sub _normalize_property_description1 { my $class = shift; my $property_name = shift; my $property_data = shift; my $class_data = shift || $class; my $class_name = $class_data->{class_name}; my %old_property = %$property_data; my %new_class = %$class_data; if (exists $old_property{unrecognized_meta_attributes}) { %old_property = (%{delete $old_property{unrecognized_meta_attributes}}, %old_property); } delete $old_property{source}; if ($old_property{implied_by} and $old_property{implied_by} eq $property_name) { $class->warning_message("Cleaning up odd self-referential 'implied_by' on $class_name $property_name"); delete $old_property{implied_by}; } # Only 1 of is_abstract, is_concrete or is_final may be set { no warnings 'uninitialized'; my $modifier_sum = $old_property{is_abstract} + $old_property{is_concrete} + $old_property{is_final}; if ($modifier_sum > 1) { Carp::confess("abstract/concrete/final are mutually exclusive. Error in class definition for $class_name property $property_name!"); } elsif ($modifier_sum == 0) { $old_property{is_concrete} = 1; } } my %new_property = ( class_name => $class_name, property_name => $property_name, ); for my $mapping ( [ property_type => qw/resolution/], [ class_name => qw//], [ property_name => qw//], [ column_name => qw/sql/], [ constraint_name => qw//], [ data_length => qw/len/], [ data_type => qw/type is isa is_a/], [ calculated_default => qw//], [ default_value => qw/default value/], [ valid_values => qw//], [ example_values => qw//], [ doc => qw/description/], [ is_optional => qw/is_nullable nullable optional/], [ is_transient => qw//], [ is_volatile => qw//], [ is_constant => qw//], [ is_classwide => qw/is_class_wide/], [ is_delegated => qw//], [ is_calculated => qw//], [ is_mutable => qw//], [ is_transactional => qw//], [ is_abstract => qw//], [ is_concrete => qw//], [ is_final => qw//], [ is_many => qw//], [ is_deprecated => qw//], [ is_undocumented => qw//], [ is_numeric => qw//], [ is_id => qw//], [ id_by => qw//], [ id_class_by => qw//], [ specify_by => qw//], [ order_by => qw//], [ access_as => qw//], [ via => qw//], [ to => qw//], [ where => qw/restrict filter/], [ implied_by => qw//], [ calculate => qw//], [ calculate_from => qw//], [ calculate_perl => qw/calc_perl/], [ calculate_sql => qw/calc_sql/], [ calculate_js => qw//], [ reverse_as => qw/reverse_id_by im_its/], [ is_legacy_eav => qw//], [ is_dimension => qw//], [ is_specified_in_module_header => qw//], [ position_in_module_header => qw//], [ singular_name => qw//], [ plural_name => qw//], ) { my $primary_field_name = $mapping->[0]; my $found_key; foreach my $key ( @$mapping ) { if (exists $old_property{$key}) { if ($found_key) { my @keys = grep { exists $old_property{$_} } @$mapping; Carp::croak("Invalid class definition for $class_name in property '$property_name'. The keys " . join(', ',$found_key,@keys) . " are all synonyms for $primary_field_name"); } $found_key = $key; } } if ($found_key) { $new_property{$primary_field_name} = delete $old_property{$found_key}; } elsif (exists $UR::Object::Property::defaults{$primary_field_name}) { $new_property{$primary_field_name} = $UR::Object::Property::defaults{$primary_field_name}; } } if (my $data = delete $old_property{delegate}) { if ($data->{via} =~ /^eav_/ and $data->{to} eq 'value') { $new_property{is_legacy_eav} = 1; } else { die "Odd delegation for $property_name: " . Data::Dumper::Dumper($data); } } if ($new_property{default_value} && $new_property{calculated_default}) { die qq(Can't initialize class $class_name: Property '$new_property{property_name}' has both default_value and calculated_default specified.); } if ($new_property{calculated_default}) { if ($new_property{calculated_default} eq 1) { $new_property{calculated_default} = '__default_' . $new_property{property_name} . '__'; } my $ref = ref $new_property{calculated_default}; if ($ref and $ref ne 'CODE') { die qq(Can't initialize class $class_name: Property '$new_property{property_name}' has calculated_default specified as a $ref ref but it must be a method name or coderef.); } unless ($ref) { my $method = $class_name->can($new_property{calculated_default}); unless ($method) { die qq(Can't initialize class $class_name: Property '$new_property{property_name}' has calculated_default specified as '$new_property{calculated_default}' but method does not exist.); } $new_property{calculated_default} = $method; } } if ($new_property{id_by} && $new_property{reverse_as}) { die qq(Can't initialize class $class_name: Property '$new_property{property_name}' has both id_by and reverse_as specified.); } if ($new_property{data_type}) { if (my ($length) = ($new_property{data_type} =~ /\((\d+)\)$/)) { $new_property{data_length} = $length; $new_property{data_type} =~ s/\(\d+\)$//; } if ($new_property{data_type} =~ m/[^\w:]/) { Carp::croak("Can't initialize class $class_name: Property '" . $new_property{property_name} . "' has metadata for is/data_type that does not look like a class name ($new_property{data_type})"); } } if (%old_property) { $new_property{unrecognized_meta_attributes} = \%old_property; %new_property = (%old_property, %new_property); } return %new_property; } sub _normalize_property_description2 { my $class = shift; my $property_data = shift; my $class_data = shift || $class; my $property_name = $property_data->{property_name}; my $class_name = $property_data->{class_name}; my %new_property = %$property_data; my %new_class = %$class_data; if (grep { $_ ne 'is_calculated' && $_ ne 'calculated_default' && /calc/ } keys %new_property) { $new_property{is_calculated} = 1; } if ($new_property{via} || $new_property{to} || $new_property{id_by} || $new_property{reverse_as} ) { $new_property{is_delegated} = 1; if (defined $new_property{via} and not defined $new_property{to}) { $new_property{to} = $property_name; } } if (!defined($new_property{is_mutable})) { if ($new_property{is_delegated} or (defined $class_data->{'subclassify_by'} and $class_data->{'subclassify_by'} eq $property_name) ) { $new_property{is_mutable} = 0; } else { $new_property{is_mutable} = 1; } } # For classes that have (or pretend to have) tables, the Property objects # should get their column_name property automatically filled in my $the_data_source; if (ref($new_class{'data_source_id'}) eq 'HASH') { # This is an inline-defined data source $the_data_source = $new_class{'data_source_id'}->{'is'}; } elsif ($new_class{'data_source_id'}) { $the_data_source = $new_class{'data_source_id'}; # using local() here to save $@ doesn't work. You end up with the # error "Unknown error" if one of the parent classes of the data source has # some kind of problem my $dollarat = $@; $@ = ''; $the_data_source = UR::DataSource->get($the_data_source) || eval { $the_data_source->get() }; unless ($the_data_source) { my $error = "Can't resolve data source from value '" . $new_class{'data_source_id'} . "' in class definition for $class_name"; if ($@) { $error .= "\n$@"; } Carp::croak($error); } $@ = $dollarat; } # UR::DataSource::File-backed classes don't have table_names, but for querying/saving to # work property, their properties still have to have column_name filled in if (($new_class{table_name} or ($the_data_source and ($the_data_source->initializer_should_create_column_name_for_class_properties()))) and not exists($new_property{column_name}) # They didn't supply a column_name and not $new_property{is_transient} and not $new_property{is_delegated} and not $new_property{is_calculated} and not $new_property{is_legacy_eav} ) { $new_property{column_name} = $new_property{property_name}; if ($the_data_source and $the_data_source->table_and_column_names_are_upper_case) { $new_property{column_name} = uc($new_property{column_name}); } } if ($new_property{order_by} and not $new_property{is_many}) { die "Cannot use order_by except on is_many properties!"; } if ($new_property{specify_by} and not $new_property{is_many}) { die "Cannot use specify_by except on is_many properties!"; } if ($new_property{implied_by} and $new_property{implied_by} eq $property_name) { $class->warnings_message("New data has odd self-referential 'implied_by' on $class_name $property_name!"); delete $new_property{implied_by}; } return %new_property; } sub _make_minimal_class_from_normalized_class_description { my $class = shift; my $desc = shift; my $class_name = $desc->{class_name}; unless ($class_name) { Carp::confess("No class name specified?"); } my $meta_class_name = $desc->{meta_class_name}; die unless $meta_class_name; if ($meta_class_name ne __PACKAGE__) { unless ( $meta_class_name->isa(__PACKAGE__) ) { warn "Bogus meta class $meta_class_name doesn't inherit from UR::Object::Type?" } } # only do this when the classes match # when they do not match, the super-class has already called this by delegating to the correct subclass $class_name::VERSION = 2.0; # No BumpVersion my $self = bless { id => $class_name, %$desc }, $meta_class_name; $UR::Context::all_objects_loaded->{$meta_class_name}{$class_name} = $self; my $full_name = join( '::', $class_name, '__meta__' ); Sub::Install::reinstall_sub({ into => $class_name, as => '__meta__', code => Sub::Name::subname $full_name => sub {$self}, }); return $self; } sub _initialize_accessors_and_inheritance { my $self = shift; $self->initialize_direct_accessors; my $class_name = $self->{class_name}; my @is = @{ $self->{is} }; unless (@is) { @is = ('UR::ModuleBase') } eval "\@${class_name}::ISA = (" . join(',', map { "'$_'" } @is) . ")\n"; Carp::croak("Can't initialize \@ISA for class_name '$class_name': $@\nMaybe the class_name or one of the parent classes are not valid class names") if $@; my $namespace_mro; my $namespace_name = $self->{namespace}; if ( !$bootstrapping && !$class_name->isa('UR::Namespace') && $namespace_name && $namespace_name->isa('UR::Namespace') && $namespace_name->can('get') && (my $namespace = $namespace_name->get()) ) { $namespace_mro = $namespace->method_resolution_order; } if ($^V lt v5.9.5 && $namespace_mro && $namespace_mro eq 'c3') { warn "C3 method resolution order is not supported on Perl < 5.9.5. Reverting $namespace_name namespace to DFS."; my $namespace = $namespace_name->get(); $namespace_mro = $namespace->method_resolution_order('dfs'); } if ($^V ge v5.9.5 && $namespace_mro && mro::get_mro($class_name) ne $namespace_mro) { mro::set_mro($class_name, $namespace_mro); } return $self; } our %_init_subclasses_loaded; sub subclasses_loaded { return @{ $_init_subclasses_loaded{shift->class_name}}; } our %_inform_all_parent_classes_of_newly_loaded_subclass; sub _inform_all_parent_classes_of_newly_loaded_subclass { my $self = shift; my $class_name = $self->class_name; Carp::confess("re-initializing class $class_name") if $_inform_all_parent_classes_of_newly_loaded_subclass{$class_name}; $_inform_all_parent_classes_of_newly_loaded_subclass{$class_name} = 1; no strict 'refs'; no warnings; my @parent_classes = @{ $class_name . "::ISA" }; for my $parent_class (@parent_classes) { unless ($parent_class->can("id")) { __PACKAGE__->use_module_with_namespace_constraints($parent_class); if ($@) { die "Failed to find parent_class $parent_class for $class_name!"; } } } my @i = sort $class_name->inheritance; $_init_subclasses_loaded{$class_name} ||= []; my $last_parent_class = ""; for my $parent_class (@i) { next if $parent_class eq $last_parent_class; $last_parent_class = $parent_class; $_init_subclasses_loaded{$parent_class} ||= []; push @{ $_init_subclasses_loaded{$parent_class} }, $class_name; push @{ $parent_class . "::_init_subclasses_loaded" }, $class_name; # any index on a parent class must move to the child class # if the child class were loaded before the index is made, it is pushed down at index creation time if (my $parent_index_hashrefs = $UR::Object::Index::all_by_class_name_and_property_name{$parent_class}) { #print "PUSHING INDEXES FOR $parent_class to $class_name\n"; for my $parent_property (keys %$parent_index_hashrefs) { my $parent_indexes = $parent_index_hashrefs->{$parent_property}; my $indexes = $UR::Object::Index::all_by_class_name_and_property_name{$class_name}{$parent_property} ||= []; push @$indexes, @$parent_indexes; } } } return 1; } sub _complete_class_meta_object_definitions { my $self = shift; # track related objects my @subordinate_objects; # grab some data from the object my $class_name = $self->{class_name}; my $table_name = $self->{table_name}; # decompose the embedded complex data structures into normalized objects my $inheritance = $self->{is}; my $properties = $self->{has}; my $relationships = $self->{relationships} || []; my $constraints = $self->{constraints}; my $data_source = $self->{'data_source_id'}; my $id_properties = $self->{id_by}; my %id_property_rank; for (my $i = '0 but true'; $i < @$id_properties; $i++) { $id_property_rank{$id_properties->[$i]} = $i; } # mark id/non-id properites foreach my $pinfo ( values %$properties ) { $pinfo->{'is_id'} = $id_property_rank{$pinfo->{'property_name'}}; } # handle inheritance unless ($class_name eq "UR::Object") { no strict 'refs'; # sanity check my @expected = @$inheritance; my @actual = @{ $class_name . "::ISA" }; if (@actual and "@actual" ne "@expected") { Carp::confess("for $class_name: expected '@expected' actual '@actual'\n"); } # set @{ $class_name . "::ISA" } = @$inheritance; } if (not $data_source and $class_name->can("__load__")) { # $data_source = UR::DataSource::Default->__define__; $data_source = { is => 'UR::DataSource::Default' }; } # Create inline data source if ($data_source and ref($data_source) eq 'HASH') { $self->{'__inline_data_source_data'} = $data_source; my $ds_class = $data_source->{'is'}; my $inline_ds = $ds_class->create_from_inline_class_data($self, $data_source); $self->{'data_source_id'} = $self->{'db_committed'}->{'data_source_id'} = $inline_ds->id; } if ($self->{'data_source_id'} and !defined($self->{table_name})) { my $data_source_obj = UR::DataSource->get($self->{'data_source_id'}) || eval { $self->{'data_source_id'}->get() }; if ($data_source_obj and $data_source_obj->initializer_should_create_column_name_for_class_properties() ) { $self->{table_name} = '__default__'; } } for my $parent_class_name (@$inheritance) { my $parent_class = $parent_class_name->__meta__; unless ($parent_class) { #$DB::single = 1; $parent_class = $parent_class_name->__meta__; $self->error_message("Failed to find parent class $parent_class_name\n"); return; } # These class meta values get propogated from parent to child foreach my $inh_property ( qw(schema_name data_source_id) ) { if (not defined ($self->$inh_property)) { if (my $inh_value = $parent_class->$inh_property) { $self->{$inh_property} = $self->{'db_committed'}->{$inh_property} = $inh_value; } } } # For classes with no data source, the default for id_generator is -urinternal # For classes with a data source, autogenerate_new_object_id_for_class_name_and_rule gets called # on that data source which can use id_generator as it sees fit if (! defined $self->{id_generator}) { my $id_generator; if ($self->{data_source_id}) { if ($parent_class->data_source_id and $parent_class->data_source_id eq $self->data_source_id ) { $id_generator = $parent_class->id_generator; } } else { $id_generator = $parent_class->id_generator; } $self->{id_generator} = $self->{'db_committed'}->{id_generator} = $id_generator; } # If a parent is declared as a singleton, we are too. # This only works for abstract singletons. if ($parent_class->is_singleton and not $self->is_singleton) { $self->is_singleton($parent_class->is_singleton); } } # when we "have" an object reference, add it to the list of old-style references # also ensure the old-style property definition is complete for my $pinfo (grep { $_->{id_by} } values %$properties) { push @$relationships, $pinfo->{property_name}, $pinfo; my $id_properties = $pinfo->{id_by}; my $r_class_name = $pinfo->{data_type}; unless($r_class_name) { die sprintf("Object accessor property definition for %s::%s has an 'id_by' but no 'data_type'", $pinfo->{'class_name'}, $pinfo->{'property_name'}); } my $r_class; my @r_id_properties; for (my $n=0; $n<@$id_properties; $n++) { my $id_property_name = $id_properties->[$n]; my $id_property_detail = $properties->{$id_property_name}; unless ($id_property_detail) { #$DB::single = 1; 1; } # No data_type specified, first try parent classes for the same property name # and use their type if (!$bootstrapping and !exists($id_property_detail->{data_type})) { if (my $inh_prop = ($self->ancestry_property_metas(property_name => $id_property_name))[0]) { $id_property_detail->{data_type} = $inh_prop->data_type; } } # Didn't find one - use the data type of the ID property(s) in the class we point to unless ($id_property_detail->{data_type}) { unless ($r_class) { # FIXME - it'd be nice if we didn't have to load the remote class here, and # instead put off loading until it's necessary $r_class ||= UR::Object::Type->get($r_class_name); unless ($r_class) { Carp::confess("Unable to load $r_class_name while defining relationship ".$pinfo->{'property_name'}. " in class $class_name"); } @r_id_properties = $r_class->id_property_names; } my ($r_property) = map { my $r_class_ancestor = UR::Object::Type->get($_); my $data = $r_class_ancestor->{has}{$r_id_properties[$n]}; ($data ? ($data) : ()); } ($r_class_name, $r_class_name->__meta__->ancestry_class_names); unless ($r_property) { #$DB::single = 1; my $property_name = $pinfo->{'property_name'}; if (@$id_properties != @r_id_properties) { Carp::croak("Can't resolve relationship for class $class_name property '$property_name': " . "id_by metadata has " . scalar(@$id_properties) . " items, but remote class " . "$r_class_name only has " . scalar(@r_id_properties) . " ID properties\n"); } else { my $r_id_property = $r_id_properties[$n] ? "'$r_id_properties[$n]'" : '(undef)'; Carp::croak("Can't resolve relationship for class $class_name property '$property_name': " . "Class $r_class_name does not have an ID property named $r_id_property, " . "which would be linked to the local property '".$id_properties->[$n]."'\n"); } } $id_property_detail->{data_type} = $r_property->{data_type}; } } next; } # make old-style (bc4nf) property objects in the default way my %property_objects; for my $pinfo (values %$properties) { my $property_name = $pinfo->{property_name}; my $property_subclass = $pinfo->{property_subclass}; # Acme::Employee::Attribute::Name is a bc6nf attribute # extends Acme::Employee::Attribute # extends UR::Object::Attribute # extends UR::Object my @words = map { ucfirst($_) } split(/_/,$property_name); #@words = $self->namespace->get_vocabulary->convert_to_title_case(@words); my $bridge_class_name = $class_name . "::Attribute::" . join('', @words); # Acme::Employee::Attribute::Name::Type is both the class definition for the bridge, # and also the attribute/property metadata for my $property_meta_class_name = $bridge_class_name . "::Type"; # define a new class for the above, inheriting from UR::Object::Property # all of the "attributes_have" get put into the class definition # call the constructor below on that new class #UR::Object::Type->__define__( ## class_name => $property_meta_class_name, # is => 'UR::Object::Property', # TODO: go through the inheritance # has => [ # @{ $class_name->__meta__->{attributes_have} } # ] #) my ($singular_name,$plural_name); unless ($pinfo->{plural_name} and $pinfo->{singular_name}) { require Lingua::EN::Inflect; if ($pinfo->{is_many}) { $plural_name = $pinfo->{plural_name} ||= $pinfo->{property_name}; $pinfo->{singular_name} = Lingua::EN::Inflect::PL_V($plural_name); } else { $singular_name = $pinfo->{singular_name} ||= $pinfo->{property_name}; $pinfo->{plural_name} = Lingua::EN::Inflect::PL($singular_name); } } my $property_object = UR::Object::Property->__define__(%$pinfo, id => $class_name . "\t" . $property_name); unless ($property_object) { $self->error_message("Error creating property $property_name for class " . $self->class_name . ": " . $class_name->error_message); for $property_object (@subordinate_objects) { $property_object->unload } $self->unload; return; } $property_objects{$property_name} = $property_object; push @subordinate_objects, $property_object; } if ($constraints) { my $property_rule_template = UR::BoolExpr::Template->resolve('UR::Object::Property','class_name','property_name'); my $n = 1; for my $unique_set (sort { $a->{sql} cmp $b->{sql} } @$constraints) { my ($name,$properties,$group,$sql); if (ref($unique_set) eq "HASH") { $name = $unique_set->{name}; $properties = $unique_set->{properties}; $sql = $unique_set->{sql}; $name ||= $sql; } else { $properties = @$unique_set; $name = '(unnamed)'; $n++; } for my $property_name (sort @$properties) { my $prop_rule = $property_rule_template->get_rule_for_values($class_name,$property_name); my $property = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $prop_rule); unless ($property) { Carp::croak("Constraint '$name' on class $class_name requires unknown property '$property_name'"); } } } } for my $obj ($self,@subordinate_objects) { #use Data::Dumper; no strict; my %db_committed = %$obj; delete @db_committed{@keys_to_delete_from_db_committed}; $obj->{'db_committed'} = \%db_committed; }; unless ($self->generate) { $self->error_message("Error generating class " . $self->class_name . " as part of creation : " . $self->error_message); for my $property_object (@subordinate_objects) { $property_object->unload } $self->unload; return; } if (my $extra = $self->{extra}) { # some class characteristics may be only present in subclasses of UR::Object # we handle these at this point, since the above is needed for bootstrapping my %still_not_found; for my $key (sort keys %$extra) { if ($self->can($key)) { $self->$key($extra->{$key}); } else { $still_not_found{$key} = $extra->{$key}; } } if (%still_not_found) { $DB::single = 1; Carp::confess("BAD CLASS DEFINITION for $class_name. Unrecognized properties: " . Data::Dumper::Dumper(%still_not_found)); } } $self->__signal_change__("load"); my @i = $class_name->inheritance; for my $parent_class_name (@i) { if ($parent_class_name->can('__signal_observers__')) { $parent_class_name->__signal_observers__('subclass_loaded', $class_name); } } # The inheritance method is high overhead because of the number of times it is called. # Cache on a per-class basis. if (grep { $_ eq '' } @i) { print "$class_name! @{ $self->{is} }"; $class_name->inheritance; } Carp::confess("Odd inheritance @i for $class_name") unless $class_name->isa('UR::Object'); my $src1 = " return shift->SUPER::inheritance(\@_) if ( (ref(\$_[0])||\$_[0]) ne '$class_name'); return (" . join(", ", map { "'$_'" } (@i)) . ")"; my $src2 = qq|sub ${class_name}::inheritance { $src1 }|; eval $src2 unless $class_name eq 'UR::Object'; die $@ if $@; $self->{'_property_meta_for_name'} = \%property_objects; # return the new class object return $self; } # write the module from the existing data in the class object sub generate { my $self = shift; return 1 if $self->{'generated'}; #my %params = @_; # Doesn't seem to be used below... # The follwing code will override a lot intentionally. # Supress the warning messages. no warnings; # the class that this object represents # the class that we're going to generate # the "new class" my $class_name = $self->class_name; # this is done earlier in the class definition process in _make_minimal_class_from_normalized_class_description() my $full_name = join( '::', $class_name, '__meta__' ); Sub::Install::reinstall_sub({ into => $class_name, as => '__meta__', code => Sub::Name::subname $full_name => sub {$self}, }); my @parent_class_names = $self->parent_class_names; do { no strict 'refs'; if (@{ $class_name . '::ISA' }) { #print "already have isa for class_name $class_name: " . join(",",@{ $class_name . '::ISA' }) . "\n"; } else { no strict 'refs'; @{ $class_name . '::ISA' } = @parent_class_names; #print "setting isa for class_name $class_name: " . join(",",@{ $class_name . '::ISA' }) . "\n"; }; }; my ($props, $cols) = ([], []); # for _all_properties_columns() $self->{_all_properties_columns} = [$props, $cols]; my $id_props = []; # for _all_id_properties() $self->{_all_id_properties} = $id_props; # build the supplemental classes for my $parent_class_name (@parent_class_names) { next if $parent_class_name eq "UR::Object"; if ($parent_class_name eq $class_name) { Carp::confess("$class_name has parent class list which includes itself?: @parent_class_names\n"); } my $parent_class_meta = UR::Object::Type->get(class_name => $parent_class_name); unless ($parent_class_meta) { #$DB::single = 1; $parent_class_meta = UR::Object::Type->get(class_name => $parent_class_name); Carp::confess("Cannot generate $class_name: Failed to find class meta-data for base class $parent_class_name."); } unless ($parent_class_meta->generated()) { $parent_class_meta->generate(); } unless ($parent_class_meta->{_all_properties_columns}) { Carp::confess("No _all_properties_columns for $parent_class_name?"); } # inherit properties and columns my ($p, $c) = @{ $parent_class_meta->{_all_properties_columns} }; push @$props, @$p if $p; push @$cols, @$c if $c; my $id_p = $parent_class_meta->{_all_id_properties}; push @$id_props, @$id_p if $id_p; } # set up accessors/mutators for properties my @property_objects = UR::Object::Property->get(class_name => $self->class_name); my @id_property_objects = $self->direct_id_property_metas; my %id_property; for my $ipo (@id_property_objects) { $id_property{$ipo->property_name} = 1; } if (@id_property_objects) { $id_props = []; for my $ipo (@id_property_objects) { push @$id_props, $ipo->property_name; } } my $has_table; my @parent_classes = map { UR::Object::Type->get(class_name => $_) } @parent_class_names; for my $co ($self, @parent_classes) { if ($co->table_name) { $has_table = 1; last; } } my $data_source_obj = $self->data_source; my $columns_are_upper_case; if ($data_source_obj) { $columns_are_upper_case = $data_source_obj->table_and_column_names_are_upper_case; } my @sort_list = map { [$_->property_name, $_] } @property_objects; for my $sorted_item ( sort { $a->[0] cmp $b->[0] } @sort_list ) { my $property_object = $sorted_item->[1]; if ($property_object->column_name) { push @$props, $property_object->property_name; push @$cols, $columns_are_upper_case ? uc($property_object->column_name) : $property_object->column_name; } } # set the flag to prevent this from occurring multiple times. $self->generated(1); # read in filesystem package if there is one #$self->use_filesystem_package($class_name); # Let each class in the inheritance hierarchy do any initialization # required for this class. Note that the _init_subclass method does # not call SUPER::, but relies on this code to find its parents. This # is the only way around a sparsely-filled multiple inheritance tree. # TODO: Replace with $class_name->EVERY::LAST::_init_subclass() #unless ( # $bootstrapping # and # $UR::Object::_init_subclass->{$class_name} #) { my @inheritance = $class_name->inheritance; my %done; for my $parent (reverse @inheritance) { my $initializer = $parent->can("_init_subclass"); next unless $initializer; next if $done{$initializer}; $initializer->($class_name,$class_name) or die "Parent class $parent failed to initialize subclass " . "$class_name :" . $parent->error_message; $done{$initializer} = 1; } } unless ($class_name->isa("UR::Object")) { print Data::Dumper::Dumper('@C::ISA',\@C::ISA,'@B::ISA',\@B::ISA); } # ensure the class is generated die "Error in module for $class_name. Resulting class does not appear to be generated!" unless $self->generated; # ensure the class inherits from UR::Object die "$class_name does not inherit from UR::Object!" unless $class_name->isa("UR::Object"); return 1; } 1; Initializer.pod100664023532023421 5565312544604516 20105 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type=pod =head1 NAME UR::Object::Type::Initializer - Class definition syntax =head1 SYNOPSIS UR::Object::Type->define( class_name => 'Namespace::MyClass', id_by => 'my_class_id', has => ['prop_a', 'prop_b'] ); UR::Object::Type->define( class_name => 'Namespace::MyChildClass', is => 'Namespace::MyClass', has => [ 'greeting' => { is => 'String', is_optional => 0, valid_values => ["hello","good morning","good evening"], default_value => 'hello' }, ], ); UR::Object::Type->define( class_name => 'Namespace::Helper', id_by => 'helper_id', has => [ 'my_class_id' => { is => 'String', is_optional => 0 }, 'my_class' => { is => 'Namespace::MyClass', id_by => 'my_class_id' }, 'my_class_a' => { via => 'my_class', to => 'prop_a' }, ], has_optional => [ 'other_attribute' => { is => 'Integer' } ], data_source => 'Namespace::DataSource::DB', table_name => 'HELPERS', ); UR::Object::Type->define( class_name => 'Namespace::Users', id_by => ['uid'], has => [ 'login','passwd','gid','name','home','shell'], data_source => { is => 'UR::DataSource::File', file => '/etc/passwd', column_order => ['login','passwd','uid','gid','name','home','shell', skip_first_line => 0, delimiter => ':' }, id_generator => '-uuid', ); =head1 DESCRIPTION Defining a UR class is like drawing up a blueprint of what a particular kind of object will look like. That blueprint includes the properties these objects will have, what other classes the new class inherits from, and where the source data comes from such as a database or file. =head2 The Simplest Class The simplest class definition would look like this: use UR; class Thing {}; You can create an instance of this class like this: my $thing_object = Thing->create(); Instances of this class have no properties, no backing storage location, and no inheritance. Actually, none of those statements are fully true, but we'll come back to that later... =head2 A Little Background After using UR, or another class that inherits from UR::Namespace, the above "class" syntax above can be used to define a class. The equivalent, more plain-Perl way to define a class is like this: UR::Object::Type->define( class_name => 'Thing', # the remainder of the class definition would go here, if there were any ); Classes become instances of another class called L. It has a property called class_name that contains the package that instances of these objects are blessed into. Class properties are also instances of a class called L, and those properties have properties (also UR::Object::Properties) that describe it, such as the type of data it holds, and its length. In fact, all the metadata about classes, properties, relationships, inheritance, data sources and contexts are available as instances of metadata classes. You can get information about any class currently available from the command line with the command ur show properties Thing with the caveat that "currently available" means you need to be under a namespace directory that contains the class you're describing. =head2 Making Something Useful class Vehicle { id_by => 'serial_number', has => ['color', 'weight'], has_optional => ['license_plate'], }; Here we have a basic class definition for a thing we're calling a Vehicle. It has 4 properties: serial_number, color, weight and license_plate. Three of these properties are required, meaning that when you create one, you must give a value for those properties; it is similar to a 'NOT NULL' constraint on a database column. The serial_number property is an ID property of the class, meaning that no two instances (objects) of that class can exist with the same serial_number; it is similar to having a UNIQUE index on that column (or columns) in a database. Not all vehicles have license plates, so it is optional. After that, you've effectively created five object instances. One UR::Object::Type identified by its class_name being 'Vehicle', and four UR::Object::Property objects identified by the pairs of class_name and property_name. For these four properties, class_name is always 'Vehicle' and property name is one each of serial_number, color, weight and license_plate. Objects always have one property that is called 'id'. If you have only one property in the id_by section, then the 'id' property is effectively an alias for it. If you have several id_by properties, then the 'id' property becomes an amalgamation of the directly named id properties such that no two objects of that class will have the same 'id'. If there are no id_by properties given (including MyClass above that doesn't have _any_ properties), then an implicit 'id' property will get created. Instances of that class will have an 'id' generated internally by an algorithm. Finally, if the class has more than one ID property, none of them may be called 'id', since that name will be reserved for the amalgamated-value property. You can control how IDs get autogenerated with the class' id_generator metadata. For classes that save their results in a relational database, it will get new IDs from a sequence (or some equivalent mechanism for databases that do not support sequences) based on the class' table name. If you want to force the system to use some specific sequence, for example if many classes should use the same sequence generator, then put the name of this sequence in. If the id_generator begins with a dash (-), it indicates a method should be called to generate a new ID. For example, if the name is "-uuid", then the system will call the internal method C<$class_meta->autogenerate_new_object_id_uuid>. nd will make object IDs as hex string UUIDs. The default value is '-urinternal' which makes an ID string composed of the hostname, process ID, the time the program was started and an increasing integer. If id_generator is a subroutine reference, then the sub will be called with the class metaobject and creation BoolExpr passed as parameters. You'll find that the parser for class definitions is pretty accepting about the kinds of data structures it will take. The first thing after class is used as a string to name the class. The second thing is a hashref containing key/value pairs. If the value part of the pair is a single string, as the id_by is in the Vehicle class definition, then one property is created. If the value portion is an arrayref, then each member of the array creates an additional property. =head2 Filling in the Details That same class definition can be made this way: class Vehicle { id_by => [ serial_number => { is => 'String', len => 25 }, ], has => [ color => { is => 'String' }, weight => { is => 'Number' }, license_plate => { is => 'String', len => 8, is_optional => 1 }, ], }; Here we've more explicitly defined the class' properties by giving them a type. serial_number and license_number are given a maximum length, and license_number is declared as optional. Note that having a 'has_optional' section is the same as explicitly putting 'is_optional => 1' for all those properties. The same shortcut is supported for the other boolean properties of UR::Object::Property, such as is_transient, is_mutable, is_abstract, etc. The type system is pretty lax in that there's nothing stopping you from using the method for the property to assign a string into a property declared 'Number'. Type, length, is_optional constraints are checked by calling C<__errors__()> on the object, and indirectly when data is committed back to its data source. =head2 Inheritance class Car { is => 'Vehicle', has => [ passenger_count => { is => 'Integer', default_value => 0 }, transmission_type => { is => 'String', valid_values => ['manual','automatic','cvt'] }, ], }; my $car = Car->create(color => 'blue', serial_number => 'abc123', transmission_type => 'manual'); Here we define another class called Car. It inherits from Vehicle, meaning that all the properties that apply to Vehicle instances also apply to Car instances. In addition, Car instances have two new properties. passenger_count has a default value of 0, and transmission_type is constrained to three possible values. =head2 More class properties Besides property definitions, there are other things that can be specified in a class definition. =over 4 =item is Used to name the parent class(es). Single inheritance can be specified by just listing the name of the parent class as a string. Multiple inheritance is specified by an arrayref containing the parent class names. If no 'is' is listed, then the class will inherit from 'UR::Entity' =item doc A single string to list some short, useful documentation about the class. =item data_source A string to list the data source ID. For classes with no data_source, the only objects get() can return are those that had previously been instantiated with create() or define() earlier in the program, and they do not get saved anywhere during a commit(). They do, however, exist in the object cache during the program's execution. data_source can also be a hashref to define a data source in line with the class definition. See below for more information about L. =item table_name When the class' data source is some kind of database, C Specifies the name of the table where this class' data is stored to. =item select_hint Some relational databases use hints as a way of telling the query optimizer to behave differently than usual. These hints are specified inside comments like this: /* the hint */ If the class is the primary class of a query, and it has a hint, then the hint will appear after the word 'select' in the SQL. =item join_hint If the class is part of a query where it is joined, then its hint will be added to the hints already part of the query. The primary table's hint will be first, followed by the joined class' hints in the order they are joined. All the hints are separated by a single space. =item is_abstract A flag indicating that no instances of this class may be instantiated, instead it is used as a parent of other classes. =item sub_classification_method_name Holds the name of a method that is called whenever new instances of the class are loaded from a data source. This method will be called with two arguments: the name of the class the get() was called on, and the object instance being loaded. The method should return the complete name of a subclass the object should be blessed into. =item sub_classification_property_name Works like 'sub_classification_method_name', except that the value of the property is directly used to subclass the loaded object. =back =head2 Properties properties C will print out an exhaustive list of all the properties of a Class Property. A class' properties are declared in the 'id_by' or one of the 'has' sections. Some of the more important ones: =over 4 =item class_name The name of the class this property belongs to. =item property_name The name of the property. 'property_name' and 'class_name' do not actually appear in the hashref that defines the property inside a class definition, though they are properties of UR::Object::Property instances. =item is Specifies the data type of this property. Basic types include 'String', 'Integer', 'Float'. Relationships between classes are defined by having the name of another class here. See the Relationships section of L for more information. Object properties do not normally hold Perl references to other objects, but you may use 'ARRAY' or 'HASH' here to indicate that the object will store the reference directly. Note that these properties are not usually saveable to outside data sources. =item data_type A synonym for 'is' =item len Specifies the maximum length of the data, usually in bytes. =item doc A space for useful documentation about the property =item default_value The value a property will have if it is not specified when the object is created. If used on a property that is 'via' another property (see the Indirect Properties section below), it can trigger creation of a referent object. =item calculated_default A name of an instance method or a code ref to be used to resolve a default value during object creation. The instance method will be called immediately after UR creates the initial entity so the method will have access to other parameters used during creation. Specifying C 1> is equivalent to, calculated_default => '__default_' . $prop_name . '__' and is meant to establish a naming convention without requiring it. =item is_mutable A flag indicating that this property can be changed. It is the default state of a property. Set this to 0 in the property definition if the property is not changeable after the object is created. =item is_constant A flag indicating that the value of this property may not be changed after the object is created. It is a synonym for having is mutable = 0 =item is_many Indicates that this returns a list of values. Usually used with reverse_as properties. =item is_optional Indicates that this property can hold the value undef. =back =head3 Calculated Properties =over =item is_calculated A flag indicating that the value of this property is determined from a function. =item calculate_from A listref of other property names used by the calculation =item calculate A specification for how the property is to be calculated in Perl. =over 6 =item * if the value is a coderef, it will be called when that property is accessed, and the first argument will be the object instance being acted on. =item * the value may be a string containing Perl code that is eval-ed when the accessor is called. The Perl code can refer to $self, which will hold the correct object instance during execution of that code. Any properties listed in the 'calculate_from' list will also be initialized =item * The special value 'sum' means that the values of all the properties in the calculate_from list are added together and returned =back Any property can be effectively turned into a calculated property by defining a method with the same name as the property. =back =head3 Database-backed properties =over 4 =item column_name For classes whose data is stored in a database table (meaning the class has a data_source), the column_name holds the name of the database column in its table. In the default case, the column_name is the same as the 'property_name'. =item calc_sql Specifies that this property is calculated, and its value is a string containing SQL code inserted into that property's "column" in the SELECT clause =back =head2 Relation Properties Some properties are not used to hold actual data, but instead describe some kind of relationship between two classes. For example: class Person { id_by => 'person_id', has => ['name'], }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; $person = Person->create(person_id => 1, name => 'Bob'); $thing = Thing->create(thing_id => 2, owner_id => 1); Here, Thing has a property called C. It implicitly defines a property called C. C becomes a read-only property that returns an object of type Person by using the object's value for the C property, and looking up a Person object where its ID matches. In the above case, C<$thing-Eowner> will return the same object that C<$person> contains. Indirect properties can also link classes with multiple ID properties. class City { id_by => ['name', 'state'] }; class Location { has => [ city => { is => 'String' }, state => { is => 'String' }, cityobj => { is => 'City', id_by => ['city', 'state' ] }, ], }; Note that the order the properties are linked must match in the relationship property's C and the related class's C =head2 Reverse Relationships When one class has a relation property to another, the target class can also define the converse relationship. In this case, OtherClass is the same as the first L example where the relationship from OtherClass to MyClass, but we also define the relationship in the other direction, from MyClass to OtherClass. Many Things can point back to the same Person. class Person { id_by => 'person_id', has => ['name'], has_many => [ things => { is => 'Thing', reverse_as => 'owner' }, ] }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; Note that the value for C needs to be the name of the relation property in the related class that would point back to "me". Yes, it's a bit obtuse, but it's the best we have for now. =head2 Indirect Properties When the property of a related object has meaning to another object, that relationship can be defined through an indirect property. Things already have owners, but it is also useful to know a Thing's owner's name. class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, owner_name => { via => 'owner', to => 'name', default_value => 'No one' }, ], }; $name = $thing->owner_name(); $name eq $person->name; # evaluates to true The values of indirect properties are not stored in the object. When the property's method is called, it looks up the related object through the accessor named in C, and on that result, returns whatever the method named in C returns. If one of these Thing objects is created by calling Thing->create(), and no value is specified for owner_id, owner or owner_name, then the system will find a Person object where its 'name' is 'No one' and assign the Thing's owner_id to point to that Person. If no matching Person is found, it will first create one with the name 'No one'. =head2 Alias Properties Sometimes it's useful to have a property that is an alias for another property, perhaps as a refactoring tool or to make the API clearer. The is accomilished by defining an indirect property where the 'via' is __self__. class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, titleholder => { via => '__self__', to => 'owner' }, ] }; In this case, 'titleholder' is an alias for the 'owner' property. titleholder can be called as a method any place owner is a valid method call. BoolExprs may refer to titleholder, but any such references will be rewrittn to 'owner' when they are normalized. =head2 Subclassing Members of an Abstract Class In some cases, objects may be loaded using a parent class, but all the objects are binned into some other subclass. class Widget { has => [ manufacturer => { is => 'String', valid_values => ['CoolCo','Vectornox'] }, ], is_abstract => 1, sub_classification_method_name => 'subclasser', }; sub Widget::subclasser { my($class,$pending_object) = @_; my $subclass = 'Widget::' . $pending_object->manufacturer; return $subclass; } class Widget::CoolCo { is => 'Widget', has => 'serial_number', }; class Widget::Vextornox { is => 'Widget', has => 'series_string', } my $cool_widget = Widget->create(manufacturer => 'CoolCo'); $cool_widget->isa('Widget::CoolCo'); # evaluates to true $cool_widget->serial_number(12345); # works $cool_widget->series_srting(); # dies In the class definition for the parent class, Widget, it is marked as being an abstract class, and the sub_classification_method_name specifies the name of a method to call whenever a new Widget object is created or loaded. That method is passed the pre-subclassed object and must return the fully qualified subclass name the object really belongs in. All the objects returned to the caller will be blessed into the appropriate subclass. Alternatively, a property can be designated to hold the fully qualified subclass name. class Widget { has => [ subclass_name => { is => 'String', valid_values => ['Widget::CoolCo', 'Widget::Vectornox'] }, ], is_abstract => 1, subclassify_by => 'subclass_name', } my $cool_widget = Widget->create(subclass_name => 'Widget::CoolCo'); $cool_widget = Widget::CoolCo->create(); # subclass_name is automatically "Widget::CoolCo" These subclass names will be saved to the data source if the class has a data source. Also, when objects of the base class are retrieved with get(), the results will be automatically put in the appropriate child class. =head2 Inline Data Sources If the data_source of a class definition is a hashref instead of a simple string, that defines an in-line data source. The only required item in that hashref is C, which declares what class this data source will be created from, such as "UR::DataSource::Oracle" or "UR::DataSource::File". From there, each type of data source will have its own requirements for what is allowed in an inline definition. For L-derived data sources, it accepts these keys corresponding to the properties of the same name: server, user, auth, owner For L data sources: server, file_list, column_order, sort_order, skip_first_line, delimiter, record_separator In addition, file is a synonym for server. For L data sources: column_order, sort_order, skip_first_line, delimiter, record_separator, required_for_get, constant_values, file_resolver In addition, resolve_path_with can replace C and accepts several formats: =over 4 =item subref A reference to a subroutine. In this case, C is a synonym for C. =item [ $subref, param1, param2, ..., paramn ] The subref will be called to resolve the path. Its arguments will be taken from the values in the rule from properties mentioned. =item [ $format, param1, param2, ..., paramn ] $format will be interpreted as an sprintf() format. The placeholders in the format will be filled in from the values in the rule from properties mentioned. =back Finally, C and C can be used together. In this case, resolve_path_with is a listref of property names, base_path is a string specifying the first part of the pathname. The final path is created by joining the base_path and all the property's values together with '/', as in join('/', $base_path, param1, param2, ..., paramn ) =head1 SEE ALSO L, L, L InternalAPI.pm100664023532023421 16763112544604516 17602 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Typepackage UR::Object::Type; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION; use Sys::Hostname; use Cwd; use Scalar::Util qw(blessed); use Sub::Name; our %meta_classes; our $bootstrapping = 1; our @partially_defined_classes; our $pwd_at_compile_time = cwd(); # each method which caches data on the class for properties stores its hash key here # when properties mutate this is cleared our @cache_keys; sub property_metas { my $self = $_[0]; my @a = map { $self->property_meta_for_name($_) } $self->all_property_names(); return @a; } # Some accessor methods drawn from properties need to be overridden. # Some times because they need to operate during bootstrapping. Sometimes # because the method needs some special behavior like sorting or filtering. # Sometimes to optimize performance or cache data # This needs to remain overridden to enforce the restriction on callers sub data_source { my $self = shift; my $ds = $self->data_source_id(@_); return undef unless $ds; my $obj = UR::DataSource->get($ds) || $ds->get(); return $obj; } sub ancestry_class_metas { #my $rule_template = UR::BoolExpr::Template->resolve(__PACKAGE__,'id'); # Can't use the speed optimization of getting a template here. Using the Context to get # objects here causes endless recursion during bootstrapping map { __PACKAGE__->get($_) } shift->ancestry_class_names; #return map { $UR::Context::current->get_objects_for_class_and_rule(__PACKAGE__, $_) } # map { $rule_template->get_rule_for_values($_) } # shift->ancestry_class_names; } our $PROPERTY_META_FOR_NAME_TEMPLATE; push @cache_keys, '_property_meta_for_name'; sub property_meta_for_name { my ($self, $property_name) = @_; return unless $property_name; if (index($property_name,'.') != -1) { my @chain = split(/\./,$property_name); my $last_class_meta = $self; my $last_class_name = $self->id; my @pmeta; for my $full_link (@chain) { my ($link) = ($full_link =~ /^([^\-\?]+)/); my $property_meta = $last_class_meta->property_meta_for_name($link); push @pmeta, $property_meta; last if $link eq $chain[-1]; my @joins = UR::Object::Join->resolve_chain($last_class_name, $link); return unless @joins; $last_class_name = $joins[-1]{foreign_class}; $last_class_meta = $last_class_name->__meta__; } return unless (@pmeta and $pmeta[-1]); return @pmeta if wantarray; return $pmeta[-1]; } my $pos = index($property_name,'-'); if ($pos != -1) { $property_name = substr($property_name,0,$pos); } if (exists($self->{'_property_meta_for_name'}) and $self->{'_property_meta_for_name'}->{$property_name}) { return $self->{'_property_meta_for_name'}->{$property_name}; } $PROPERTY_META_FOR_NAME_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name'); my $property; for my $class ($self->class_name, $self->ancestry_class_names) { my $rule = $PROPERTY_META_FOR_NAME_TEMPLATE->get_rule_for_values($class, $property_name); $property = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $rule); if ($property) { return $self->{'_property_meta_for_name'}->{$property_name} = $property; } } return; } # A front-end for property_meta_for_name, but # will translate the generic 'id' property into the class' real ID property, # if it's not called 'id' sub _concrete_property_meta_for_class_and_name { my($self,$property_name) = @_; my @property_metas = $self->property_meta_for_name($property_name); for (my $i = 0; $i < @property_metas; $i++) { if ($property_metas[$i]->id eq "UR::Object\tid" and $property_name !~ /\./) #If we're looking at a foreign object's id, can't replace with our own { # This is the generic id property. Remap it to the class' real ID property name my @id_properties = $self->id_property_names; if (@id_properties == 1 and $id_properties[0] eq 'id') { next; # this class doesn't have any other ID properties } #return map { $self->_concrete_property_meta_for_class_and_name($_) } @id_properties; my @remapped = map { $self->_concrete_property_meta_for_class_and_name($_) } @id_properties; splice(@property_metas, $i, 1, @remapped); } } return @property_metas; } sub _flatten_property_name { my ($self, $name) = @_; my $flattened_name = ''; my @add_keys; my @add_values; my @meta = $self->property_meta_for_name($name); for my $meta (@meta) { my @joins = $meta->_resolve_join_chain(); for my $join (@joins) { if ($flattened_name) { $flattened_name .= '.'; } $flattened_name .= $join->{source_name_for_foreign}; if (my $where = $join->{where}) { $flattened_name .= '-' . $join->sub_group_label; my $join_class = $join->{foreign_class}; my $bx2 = UR::BoolExpr->resolve($join_class,@$where); my $bx2_flat = $bx2->flatten(); # recurses through this my ($bx2_flat_template, @values) = $bx2_flat->template_and_values(); my @keys = @{ $bx2_flat_template->{_keys} }; for my $key (@keys) { next if substr($key,0,1) eq '-'; my $full_key = $flattened_name . '?.' . $key; push @add_keys, $full_key; push @add_values, shift @values; } if (@values) { Carp:confess("Unexpected mismatch in count of keys and values!"); } } } } return ($flattened_name, \@add_keys, \@add_values); }; our $DIRECT_ID_PROPERTY_METAS_TEMPLATE; sub direct_id_property_metas { my $self = _object(shift); $DIRECT_ID_PROPERTY_METAS_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name', 'is_id >='); my $class_name = $self->class_name; my @id_property_objects = map { $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $_) } map { $DIRECT_ID_PROPERTY_METAS_TEMPLATE->get_rule_for_values($class_name, $_, 0) } @{$self->{'id_by'}}; my $sort_sub = sub ($$) { return $_[0]->is_id cmp $_[1]->is_id }; @id_property_objects = sort $sort_sub @id_property_objects; if (@id_property_objects == 0) { @id_property_objects = $self->property_meta_for_name("id"); } return @id_property_objects; } sub parent_class_names { my $self = shift; return @{ $self->{is} }; } # If $property_name represents an alias-type property (via => '__self__'), # then return a string with all the aliases removed push @cache_keys, '_resolve_property_aliases'; sub resolve_property_aliases { my($self,$property_name) = @_; return unless $property_name; unless ($self->{'_resolve_property_aliases'} && $self->{'_resolve_property_aliases'}->{$property_name}) { $self->{'_resolve_property_aliases'} ||= {}; my @property_metas = $self->property_meta_for_name($property_name); my @property_names; if (@property_metas) { @property_names = map { $_->alias_for } @property_metas; } else { # there was a problem resolving the chain of properties # This happens in the case of an object accessor (is => 'Some::Class') without an id_by my @split_names = split(/\./,$property_name); my $prop_meta = $self->property_meta_for_name(shift @split_names); return unless $prop_meta; my $foreign_class = $prop_meta->data_type && eval { $prop_meta->data_type->__meta__}; return unless $foreign_class; @property_names = ( $prop_meta->alias_for, $foreign_class->resolve_property_aliases(join('.', @split_names))); } $self->{'_resolve_property_aliases'}->{$property_name} = join('.', @property_names); } return $self->{'_resolve_property_aliases'}->{$property_name}; } push @cache_keys, '_id_property_names'; sub id_property_names { # FIXME Take a look at id_property_names and all_id_property_names. # They look extremely similar, but tests start dying if you replace one # with the other, or remove both and rely on the property's accessor method my $self = _object(shift); unless ($self->{'_id_property_names'}) { my @id_by; unless ($self->{id_by} and @id_by = @{ $self->{id_by} }) { foreach my $parent ( @{ $self->{'is'} } ) { my $parent_class = $parent->class->__meta__; next unless $parent_class; @id_by = $parent_class->id_property_names; last if @id_by; } } $self->{'_id_property_names'} = \@id_by; } return @{$self->{'_id_property_names'}}; } push @cache_keys, '_all_id_property_names'; sub all_id_property_names { # return shift->id_property_names(@_); This makes URT/t/99_transaction.t fail my $self = shift; unless ($self->{_all_id_property_names}) { my ($tmp,$last) = ('',''); $self->{_all_id_property_names} = [ grep { $tmp = $last; $last = $_; $tmp ne $_ } sort map { @{ $_->{id_by} } } map { __PACKAGE__->get($_) } ($self->class_name, $self->ancestry_class_names) ]; } return @{ $self->{_all_id_property_names} }; } sub direct_id_column_names { my $self = _object(shift); my @id_column_names = map { $_->column_name } $self->direct_id_property_metas; return @id_column_names; } sub ancestry_table_names { my $self = _object(shift); my @inherited_table_names = grep { defined($_) } map { $_->table_name } $self->ancestry_class_metas; return @inherited_table_names; } sub all_table_names { my $self = _object(shift); my @table_names = grep { defined($_) } ( $self->table_name, $self->ancestry_table_names ); return @table_names; } sub first_table_name { my $self = _object(shift); if ($self->{_first_table_name}) { return $self->{first_table_name}; } my @classes = ($self); while(@classes) { my $co = shift @classes; if (my $table_name = $co->table_name) { $self->{first_table_name} = $table_name; return $table_name; } my @parents = map { $_->__meta__ } @{$co->{'is'}}; push @classes, @parents; } return; } sub ancestry_class_names { my $self = shift; if ($self->{_ordered_inherited_class_names}) { return @{ $self->{_ordered_inherited_class_names} }; } my $ordered_inherited_class_names = $self->{_ordered_inherited_class_names} = [ @{ $self->{is} } ]; my @unchecked = @$ordered_inherited_class_names; my %seen = ( $self->{class_name} => 1 ); while (my $ancestor_class_name = shift @unchecked) { next if $seen{$ancestor_class_name}; $seen{$ancestor_class_name} = 1; my $class_meta = $ancestor_class_name->__meta__; Carp::confess("Can't find meta for $ancestor_class_name!") unless $class_meta; next unless $class_meta->{is}; push @$ordered_inherited_class_names, @{ $class_meta->{is} }; unshift @unchecked, $_ for reverse @{ $class_meta->{is} }; } return @$ordered_inherited_class_names; } push @cache_keys, '_all_property_names'; sub all_property_names { my $self = shift; if ($self->{_all_property_names}) { return @{ $self->{_all_property_names} }; } my %seen = (); my $all_property_names = $self->{_all_property_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { next if $class_name eq 'UR::Object'; my $class_meta = UR::Object::Type->get($class_name); if (my $has = $class_meta->{has}) { push @$all_property_names, grep { not exists $has->{$_}{id_by} } grep { !exists $seen{$_} } sort keys %$has; foreach (@$all_property_names) { $seen{$_} = 1; } } } return @$all_property_names; } ######################################################################## # End of overridden property methods ######################################################################## sub _resolve_meta_class_name_for_class_name { my $class = shift; my $class_name = shift; #if ($class_name->isa("UR::Object::Type") or $meta_classes{$class_name} or $class_name =~ '::Type') { if ($meta_classes{$class_name} or $class_name =~ '::Type') { return "UR::Object::Type" } else { return $class_name . "::Type"; } } sub _resolve_meta_class_name { my $class = shift; my ($rule,%extra) = UR::BoolExpr->resolve_normalized($class, @_); my %params = $rule->params_list; my $class_name = $params{class_name}; return unless $class_name; return $class->_resolve_meta_class_name_for_class_name($class_name); } # This method can go away when we have the is_cached meta-property sub first_sub_classification_method_name { my $self = shift; # This may be one of many things which class meta-data should "inherit" from classes which # its instances inherit from. This value is set to the value found on the most concrete class # in the inheritance tree. return $self->{___first_sub_classification_method_name} if exists $self->{___first_sub_classification_method_name}; $self->{___first_sub_classification_method_name} = $self->sub_classification_method_name; unless ($self->{___first_sub_classification_method_name}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{___first_sub_classification_method_name} = $parent_class->sub_classification_method_name); } } return $self->{___first_sub_classification_method_name}; } # Another thing that is "inherited" from parent class metas sub subclassify_by { my $self = shift; return $self->{'__subclassify_by'} if exists $self->{'__subclassify_by'}; $self->{'__subclassify_by'} = $self->__subclassify_by; unless ($self->{'__subclassify_by'}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{'__subclassify_by'} = $parent_class->__subclassify_by); } } return $self->{'__subclassify_by'}; } sub resolve_composite_id_from_ordered_values { my $self = shift; my $resolver = $self->get_composite_id_resolver; return $resolver->(@_); } sub resolve_ordered_values_from_composite_id { my $self = shift; my $decomposer = $self->get_composite_id_decomposer; return $decomposer->(@_); } sub get_composite_id_decomposer { my $self = shift; my $decomposer; unless ($decomposer = $self->{get_composite_id_decomposer}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $decomposer = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $decomposer = sub { if (ref($_[0])) { # ID is an arrayref, or we'll throw an exception. my $id = $_[0]; my $underlying_id_count = scalar(@$id); # Handle each underlying ID, turning each into an arrayref divided by property value. my @decomposed_ids; for my $underlying_id (@$id) { push @decomposed_ids, [map { $_ eq '' ? undef : $_ } split($separator,$underlying_id)]; } # Count the property values. my $underlying_property_count = scalar(@{$decomposed_ids[0]}) if @decomposed_ids; $underlying_property_count ||= 0; # Make a list of property values, but each value will be an # arrayref of a set of values instead of a single value. my @property_values; for (my $n = 0; $n < $underlying_property_count; $n++) { $property_values[$n] = [ map { $_->[$n] } @decomposed_ids ]; } return @property_values; } else { # Regular scalar ID. no warnings 'uninitialized'; # $_[0] can be undef in some cases... return split($separator,$_[0]) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_decomposer(closure)',$decomposer); $self->{get_composite_id_decomposer} = $decomposer; } return $decomposer; } sub _resolve_composite_id_separator { # TODO: make the class pull this from its parent at creation time # and only have it dump it if it differs from its parent my $self = shift; my $separator = "\t"; for my $class_meta ($self, $self->ancestry_class_metas) { if ($class_meta->composite_id_separator) { $separator = $class_meta->composite_id_separator; last; } } return $separator; } sub get_composite_id_resolver { my $self = shift; my $resolver; unless($resolver = $self->{get_composite_id_resolver}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $resolver = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $resolver = sub { if (ref($_[0]) eq 'ARRAY') { # Determine how big the arrayrefs are. my $underlying_id_count = scalar(@{$_[0]}); # We presume that, if one value is an arrayref, the others are also, # and are of equal length. my @id; for (my $id_num = 0; $id_num < $underlying_id_count; $id_num++) { # One value per id_property on the class. # Each value is an arrayref in this case. for my $value (@_) { no warnings 'uninitialized'; # Some values in the list might be undef $id[$id_num] .= $separator if $id[$id_num]; $id[$id_num] .= $value->[$id_num]; } } return \@id; } else { no warnings 'uninitialized'; # Some values in the list might be undef return join($separator,@_) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_resolver(closure)',$resolver); $self->{get_composite_id_resolver} = $resolver; } return $resolver; } # UNUSED, BUT BETTER FOR MULTI-COLUMN FK sub composite_id_list_scalar_mix { # This is like the above, but handles the case of arrayrefs # mixing with scalar values in a multi-property id. my ($self, @values) = @_; my @id_sets; for my $value (@values) { if (@id_sets == 0) { if (not ref $value) { @id_sets = ($value); } else { @id_sets = @$value; } } else { if (not ref $value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } else { for my $new_id (@$value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } } } } if (@id_sets == 1) { return $id_sets[0]; } else { return \@id_sets; } } sub id_property_sorter { # Return a closure that sort can use to sort objects by all their ID properties # This should be the same order that an SQL query with 'order by ...' would return them my $self = shift; return $self->{'_id_property_sorter'} ||= $self->sorter(); } sub sorter { #TODO: make this take +/- indications of ascending/descending #TODO: make it into a closure for speed #TODO: there are possibilities of it sorting different than a DB on mixed numbers and alpha data my ($self,@properties) = @_; push @properties, $self->id_property_names; my $key = join("__",@properties); my $sorter = $self->{_sorter}{$key}; unless ($sorter) { my @is_numeric; my @is_descending; for my $property (@properties) { if ($property =~ m/^(-|\+)(.*)$/) { push @is_descending, $1 eq '-'; $property = $2; # yes, we're manipulating the original list element } else { push @is_descending, 0; } my $class_meta; if ($self->isa("UR::Object::Set::Type")) { # If we're a set, we want to examine the property of our members. my $subject_class = $self->class_name; $subject_class =~ s/::Set$//g; $class_meta = $subject_class->__meta__;#->property($property); } else { $class_meta = $self; } my ($pmeta,@extra) = $class_meta->_concrete_property_meta_for_class_and_name($property); if(@extra) { $pmeta = $class_meta->property($property); #a composite property (typically ID) } if ($pmeta) { my $is_numeric = $pmeta->is_numeric; push @is_numeric, $is_numeric; } elsif ($UR::initialized) { Carp::cluck("Failed to find property meta for $property on $self? Cannot produce a sorter for @properties"); push @is_numeric, 0; } else { push @is_numeric, 0; } } no warnings; # don't print a warning about undef values ...alow them to be treated as 0 or '' $sorter = $self->{_sorter}{$key} ||= sub($$) { for (my $n = 0; $n < @properties; $n++) { my $property = $properties[$n]; my @property_string = split('\.',$property); my($first,$second) = $is_descending[$n] ? ($_[1], $_[0]) : ($_[0], $_[1]); for my $current (@property_string) { $first = $first->$current; $second = $second->$current; if (!defined($second)) { return -1; } elsif (!defined($first)) { return 1; } } my $cmp = $is_numeric[$n] ? $first <=> $second : $first cmp $second; return $cmp if $cmp; } return 0; }; } Sub::Name::subname("UR::Object::Type::sorter__" . $self->class_name . '__' . $key, $sorter); return $sorter; } sub is_meta { my $self = shift; my $class_name = $self->class_name; return grep { $_ ne 'UR::Object' and $class_name->isa($_) } keys %meta_classes; } sub is_meta_meta { my $self = shift; my $class_name = $self->class_name; return 1 if $meta_classes{$class_name}; return; } # Things that can't safely be removed from the object cache. our %uncachable_types = ( ( map { $_ => 0 } keys %UR::Object::Type::meta_classes), # meta-classes are locked in the cache... 'UR::Object' => 1, # .. except for UR::Object 'UR::Object::Ghost' => 0, 'UR::DataSource' => 0, 'UR::Context' => 0, 'UR::Object::Index' => 0, ); sub is_uncachable { my $self = shift; my $class_name = $self->class_name; if (@_) { # setting the is_uncachable value return $uncachable_types{$class_name} = shift; } unless (exists $uncachable_types{$class_name}) { my $is_uncachable = 1; foreach my $type ( keys %uncachable_types ) { if ($class_name->isa($type) and ! $uncachable_types{$type}) { $is_uncachable = 0; last; } } $uncachable_types{$class_name} = $is_uncachable; unless (exists $uncachable_types{$class_name}) { die "Couldn't determine is_uncachable() for $class_name"; } } return $uncachable_types{$class_name}; } # Mechanisms for generating object IDs when none were specified at # creation time sub autogenerate_new_object_id_uuid { require Data::UUID; my $uuid = Data::UUID->new->create_hex(); $uuid =~ s/^0x//; return $uuid; } our $autogenerate_id_base_format = join(" ",Sys::Hostname::hostname(), "%s", time); # the %s gets $$ when needed our $autogenerate_id_iter = 10000; sub autogenerate_new_object_id_urinternal { my($self, $rule) = @_; my @id_property_names = $self->id_property_names; if (@id_property_names > 1) { # we really could, but it seems like if you # asked to do it, it _has_ to be a mistake. If there's a legitimate # reason, this check should be removed $self->error_message("Can't autogenerate ID property values for multiple ID property class " . $self->class_name); return; } return sprintf($autogenerate_id_base_format, $$) . " " . (++$autogenerate_id_iter); } sub autogenerate_new_object_id_datasource { my($self,$rule) = @_; my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { return $data_source->autogenerate_new_object_id_for_class_name_and_rule( $self->class_name, $rule ); } else { Carp::croak("Class ".$self->class." has id_generator '-datasource', but the class has no data source to delegate to"); } } # Support the autogeneration of unique IDs for objects which require them. sub autogenerate_new_object_id { my $self = _object($_[0]); #my $rule = shift; unless ($self->{'_resolved_id_generator'}) { my $id_generator = $self->id_generator; if (ref($id_generator) eq 'CODE') { $self->{'_resolved_id_generator'} = $id_generator; } elsif ($id_generator and $id_generator =~ m/^\-(\S+)/) { my $id_method = 'autogenerate_new_object_id_' . $1; my $subref = $self->can($id_method); unless ($subref) { Carp::croak("'$id_generator' is an invalid id_generator for class " . $self->class_name . ": Can't locate object method '$id_method' via package ".ref($self)); } $self->{'_resolved_id_generator'} = $subref; } else { # delegate to the data source my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { $self->{'_resolved_id_generator'} = sub { $data_source->autogenerate_new_object_id_for_class_name_and_rule( shift->class_name, shift ) }; } } } goto $self->{'_resolved_id_generator'}; } # from ::Object->generate_support_class our %support_class_suffixes = map { $_ => 1 } qw/Set View Viewer Ghost Iterator Value/; sub generate_support_class_for_extension { my $self = shift; my $extension_for_support_class = shift; my $subject_class_name = $self->class_name; unless ($subject_class_name) { Carp::confess("No subject class name for $self?"); } return unless defined $extension_for_support_class; if ($subject_class_name eq "UR::Object") { # Carp::cluck("can't generate $extension_for_support_class for UR::Object!\n"); # NOTE: we hit this a bunch of times when "getting" meta-data objects during boostrap. return; } unless ($support_class_suffixes{$extension_for_support_class}) { #$self->debug_message("Cannot generate a class with extension $extension_for_support_class."); return; } my $subject_class_obj = UR::Object::Type->get(class_name => $subject_class_name); unless ($subject_class_obj) { $self->debug_message("Cannot autogenerate $extension_for_support_class because $subject_class_name does not exist."); return; } my $new_class_name = $subject_class_name . "::" . $extension_for_support_class; my $class_obj; if ($class_obj = UR::Object::Type->is_loaded($new_class_name)) { # getting the subject class autogenerated the support class automatically # shortcut out return $class_obj; } no strict 'refs'; my @subject_parent_class_names = @{ $subject_class_name . "::ISA" }; my @parent_class_names = grep { UR::Object::Type->get(class_name => $_) } map { $_ . "::" . $extension_for_support_class } grep { $_->isa("UR::Object") } grep { $_ !~ /^UR::/ or $extension_for_support_class eq "Ghost" } @subject_parent_class_names; use strict 'refs'; unless (@parent_class_names) { if (UR::Object::Type->get(class_name => ("UR::Object::" . $extension_for_support_class))) { @parent_class_names = "UR::Object::" . $extension_for_support_class; } } unless (@parent_class_names) { #print Carp::longmess(); #$self->error_message("Cannot autogenerate $extension_for_support_class for $subject_class_name because parent classes (@subject_parent_class_names) do not have classes with that extension."); return; } my @id_property_names = $subject_class_obj->id_property_names; my %id_property_names = map { $_ => 1 } @id_property_names; if ($extension_for_support_class eq 'Ghost') { my $subject_class_metaobj = UR::Object::Type->get($self->meta_class_name); # Class object for the subject_class my %class_params = map { $_ => $subject_class_obj->$_ } grep { my $p = $subject_class_metaobj->property_meta_for_name($_) || Carp::croak("Can't no metadata for property '$_' of class ".$self->meta_class_name); ! $p->is_delegated and ! $p->is_calculated } $subject_class_obj->__meta__->all_property_names; delete $class_params{generated}; delete $class_params{meta_class_name}; delete $class_params{subclassify_by}; delete $class_params{sub_classification_meta_class_name}; delete $class_params{id_generator}; delete $class_params{id}; delete $class_params{is}; my $attributes_have = UR::Util::deep_copy($subject_class_obj->{attributes_have}); my $class_props = UR::Util::deep_copy($subject_class_obj->{has}); for (values %$class_props) { delete $_->{class_name}; delete $_->{property_name}; } %class_params = ( %class_params, class_name => $new_class_name, is => \@parent_class_names, is_abstract => 0, has => [%$class_props], attributes_have => $attributes_have, id_properties => \@id_property_names, ); $class_obj = UR::Object::Type->define(%class_params); } else { $class_obj = UR::Object::Type->define( class_name => $subject_class_name . "::" . $extension_for_support_class, is => \@parent_class_names, ); } return $class_obj; } sub has_table { my $self = shift; if ($bootstrapping) { return 0; } return 1 if $self->table_name; # FIXME - shouldn't this call inheritance() instead of parent_classes()? my @parent_classes = $self->parent_classes; for my $class_name (@parent_classes) { next if $class_name eq "UR::Object"; my $class_obj = UR::Object::Type->get(class_name => $class_name); if ($class_obj->table_name) { return 1; } } return; } sub most_specific_subclass_with_table { my $self = shift; return $self->class_name if $self->table_name; foreach my $class_name ( $self->class_name->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub most_general_subclass_with_table { my $self = shift; my @subclass_list = reverse ( $self->class_name, $self->class_name->inheritance ); foreach my $class_name ( $self->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub _load { my $class = shift; my $rule = shift; $rule = $rule->normalize; my $params = $rule->legacy_params_hash; # While core entity classes are actually loaded, # support classes dynamically generate for them as needed. # Examples are Acme::Employee::View::emp_id, and Acme::Equipment::Ghost # Try to parse the class name. my $class_name = $params->{class_name}; # See if the class autogenerates from another class. # i.e.: Acme::Foo::Bar might be generated by Acme::Foo unless ($class_name) { my $namespace = $params->{namespace}; if (my $data_source = $params->{data_source_id}) { $namespace = $data_source->get_namespace; } if ($namespace) { # FIXME This chunk seems to be getting called each time there's a new table/class #Carp::cluck("Getting all classes for namespace $namespace from the filesystem..."); my @classes = $namespace->get_material_classes; return $class->is_loaded($params); } Carp::confess("Non-class_name used to find a class object: " . join(', ', map { "$_ => " . (defined $params->{$_} ? "'" . $params->{$_} . "'" : 'undef') } keys %$params)); } # Besides the common case of asking for a class by its name, the next most # common thing is asking for multiple classes by their names. Rather than doing the # hard work of doing it "right" right here, just recursively call myself with each # item in that list if (ref $class_name eq 'ARRAY') { # FIXME is there a more efficient way to add/remove class_name from the rule? my $rule_without_class_name = $rule->remove_filter('class_name'); $rule_without_class_name = $rule_without_class_name->remove_filter('id'); # id is a synonym for class_name my @objs = map { $class->_load($rule_without_class_name->add_filter(class_name => $_)) } @$class_name; return $class->context_return(@objs); } # If the class is loaded, we're done. # This is an un-documented unique constraint right now. my $class_obj = $class->is_loaded(class_name => $class_name); return $class_obj if $class_obj; # Handle deleted classes. # This is written in non-oo notation for bootstrapping. no warnings; if ( $class_name ne "UR::Object::Type::Ghost" and UR::Object::Type::Ghost->can("class") and $UR::Context::current->get_objects_for_class_and_rule("UR::Object::Type::Ghost",$rule,0) ) { return; } # Check the filesystem. The file may create its metadata object. my $exception = do { local $@; eval "use $class_name"; $@; }; unless ($exception) { # If the above module was loaded, and is an UR::Object, # this will find the object. If not, it will return nothing. $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); return $class_obj if $class_obj; } if ($exception) { # We need to handle $@ here otherwise we'll see # "Can't locate UR/Object/Type/Ghost.pm in @INC" error. # We want to fall through "in the right circumstances". (my $module_path = $class_name . '.pm') =~ s/::/\//g; Carp::croak("Error while autoloading with 'use $class_name': $exception") unless ($exception =~ /Can't locate $module_path in \@INC/); # FIXME: I think other conditions here will result in silent errors. } # Parse the specified class name to check for a suffix. my ($prefix, $base, $suffix) = ($class_name =~ /^([^\:]+)::(.*)::([^:]+)/); my @parts; ($prefix, @parts) = split(/::/,$class_name); for (my $suffix_pos = $#parts; $suffix_pos >= 0; $suffix_pos--) { $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); if ($class_obj) { # the class was somehow generated while we were checking other classes for it and failing. # this can happen b/c some class with a name which is a subset of the one we're looking # for might "use" the one we want. return $class_obj if $class_obj; } my $base = join("::", @parts[0 .. $suffix_pos-1]); my $suffix = join("::", @parts[$suffix_pos..$#parts]); # See if a class exists for the same name w/o the suffix. # This may cause this function to be called recursively for # classes like Acme::Equipment::Set::View::upc_code, # which would fire recursively for three extensions of # Acme::Equipment. my $full_base_class_name = $prefix . ($base ? "::" . $base : ""); my $base_class_obj = eval { $full_base_class_name->__meta__ }; if ($base_class_obj) { # If so, that class may be able to generate a support # class. $class_obj = $full_base_class_name->__extend_namespace__($suffix); if ($class_obj) { # Autogeneration worked. # We still defer to is_loaded, since other parameters # may prevent the newly "loaded" class from being # returned. return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0) } } } # If we fall-through to this point, no class was found and no module. return; } sub use_module_with_namespace_constraints { use strict; use warnings; my $self = shift; my $target_class = shift; # If you do "use Acme; $o = Acme::Rocket->new();", and Perl finds Acme.pm # at "/foo/bar/Acme.pm", Acme::Rocket must be under /foo/bar/Acme/ # in order to be dynamically loaded. my @words = split("::",$target_class); my $path; while (@words > 1) { my $namespace_name = join("::",@words[0..$#words-1]); my $namespace_expected_module = join("/",@words[0..$#words-1]) . ".pm"; if ($path = $INC{$namespace_expected_module}) { #print "got mod $namespace_expected_module at $path for $target_class\n"; $path =~ s/\/*$namespace_expected_module//g; } else { my $namespace_obj = UR::Object::Type->is_loaded(class_name => $namespace_name); if ($namespace_obj) { eval { $path = $namespace_obj->module_directory }; if ($@) { # non-module class # don't auto-use, but don't make a lot of noise about it either } } } last if $path; pop @words; } unless ($path) { #Carp::cluck("No module_directory found for namespace $namespace_name." # . " Cannot dynamically load $target_class."); return; } $self->_use_safe($target_class,$path); my $meta = UR::Object::Type->is_loaded(class_name => $target_class); if ($meta) { return $meta; } else { return; } } sub _use_safe { use strict; use warnings; my ($self, $target_class, $expected_directory) = @_; # TODO: use some smart module to determine whether the path is # relative on the current system. if (defined($expected_directory) and $expected_directory !~ /^[\/\\]/) { $expected_directory = $pwd_at_compile_time . "/" . $expected_directory; } my $class_path = $target_class . ".pm"; $class_path =~ s/\:\:/\//g; my @INC_COPY = @INC; if ($expected_directory) { unshift @INC, $expected_directory; } my $found = ""; for my $dir (@INC) { if ($dir and (-e $dir . "/" . $class_path)) { $found = $dir; last; } } if (!$found) { # not found @INC = @INC_COPY; return; } if ($expected_directory and $expected_directory ne $found) { # not found in the specified location @INC = @INC_COPY; return; } do { local $SIG{__DIE__}; local $SIG{__WARN__}; eval "use $target_class"; }; # FIXME - if the use above failed because of a compilation error in the module we're trying to # load, then the error message below just tells the user that "Compilation failed in require" # and isn't propogating the error message about what caused the compile to fail if ($@) { #local $SIG{__DIE__}; @INC = @INC_COPY; die ("ERROR DYNAMICALLY LOADING CLASS $target_class\n$@"); } for (0..$#INC) { if ($INC[$_] eq $expected_directory) { splice @INC, $_, 1; last; } } return 1; } # sub _object # This is used to make sure that methods are called # as object methods and not class methods. # The typical case that's important is when something # like UR::Object::Type->method(...) is called. # If an object is expected in a method and it gets # a class instead, well, unpredictable things can # happen. # # For many methods on UR::Objects, the implementation # is in UR::Object. However, some of those methods # have the same name as methods in here (purposefully), # and those UR::Object methods often get the # UR::Object::Type object and call the same method, # which ends up in this file. The problem is when # those methods are called on UR::Object::Type # itself it come directly here, without getting # the UR::Object::Type object for UR::Object::Type # (confused yet?). So to fix this, we use _object to # make sure we have an object and not a class. # # Basically, we make sure we're working with a class # object and not a class name. # sub _object { return ref($_[0]) ? $_[0] : $_[0]->__meta__; } # new version gets everything, including "id" itself and object ref properties push @cache_keys, '_all_property_type_names'; sub all_property_type_names { my $self = shift; if ($self->{_all_property_type_names}) { return @{ $self->{_all_property_type_names} }; } #my $rule_template = UR::BoolExpr::Template->resolve('UR::Object::Type', 'id'); my $all_property_type_names = $self->{_all_property_type_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); #my $rule = $rule_template->get_rule_for_values($class_name); #my $class_meta = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Type',$rule); if (my $has = $class_meta->{has}) { push @$all_property_type_names, sort keys %$has; } } return @$all_property_type_names; } sub table_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to table_for_property') unless @_; my $property_name = shift; for my $class_object ( $self, $self->ancestry_class_metas ) { my $property_object = UR::Object::Property->get( class_name => $class_object->class_name, property_name => $property_name ); if ( $property_object ) { next unless $property_object->column_name; return $class_object->table_name; } } return; } sub column_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to column_for_property') unless @_; my $property_name = shift; my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$properties; $i++) { if ($properties->[$i] eq $property_name) { return $columns->[$i]; } } for my $class_object ( $self->ancestry_class_metas ) { my $column_name = $class_object->column_for_property($property_name); return $column_name if $column_name; } return; } sub property_for_column { my $self = _object(shift); Carp::croak('must pass a column_name to property_for_column') unless @_; my $column_name = lc(shift); my $data_source = $self->data_source || 'UR::DataSource'; my($table_name,$self_table_name); ($table_name, $column_name) = $data_source->_resolve_table_and_column_from_column_name($column_name); (undef, $self_table_name) = $data_source->_resolve_owner_and_table_from_table_name($self->table_name); if (! $table_name) { my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$columns; $i++) { if (lc($columns->[$i]) eq $column_name) { return $properties->[$i]; } } } elsif ($table_name and $self_table_name and lc($self_table_name) eq lc($table_name) ) { # @$properties and @$columns contain items inherited from parent classes # make sure the property we find with that name goes to this class my $property_name = $self->property_for_column($column_name); return undef unless $property_name; my $prop_meta = $self->property_meta_for_name($property_name); if ($prop_meta->class_name eq $self->class_name and lc($prop_meta->column_name) eq $column_name ) { return $property_name; } } elsif ($table_name) { for my $class_object ( $self, $self->ancestry_class_metas ) { next unless $class_object->data_source; my $class_object_table_name; (undef, $class_object_table_name) = $class_object->data_source->_resolve_owner_and_table_from_table_name($class_object->table_name); if (! $class_object_table_name or $table_name ne lc($class_object_table_name) ) { (undef, $class_object_table_name) = $class_object->data_source->parse_view_and_alias_from_inline_view($class_object->table_name); } next if (! $class_object_table_name or $table_name ne lc($class_object_table_name)); my $property_name = $class_object->property_for_column($column_name); return $property_name if $property_name; } } return; } # Methods for maintaining unique constraints # This is primarily used by the class re-writer (ur update classes-from-db), but # BoolExprs use them,too # Adds a constraint by name and property list to the class metadata. The class initializer # fills this data in via the 'constraints' key, so it shouldn't call add_unique_constraint() # directly sub add_unique_constraint { my $self = shift; unless (@_) { Carp::croak('method add_unique_constraint requires a constraint name as a parameter'); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); if (exists $constraints->{$constraint_name}) { Carp::croak("A constraint named '$constraint_name' already exists for class ".$self->class_name); } unless (@_) { Carp::croak('method add_unique_constraint requires one or more property names as parameters'); } my @property_names = @_; # Add a new constraint record push @{ $self->{'constraints'} } , { sql => $constraint_name, properties => \@property_names }; # invalidate the other cached data $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } sub remove_unique_constraint { my $self = shift; unless (@_) { Carp::croak("method remove_unique_constraint requires a constraint name as a parameter"); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); unless (exists $constraints->{$constraint_name}) { Carp::croak("There is no constraint named '$constraint_name' for class ".$self->class_name); } # Remove the constraint record for (my $i = 0; $i < @{$self->{'constraints'}}; $i++) { if ($self->{'constraints'}->[$i]->{'sql'} = $constraint_name) { splice(@{$self->{'constraints'}}, $i, 1); } } $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } # This returns a list of lists. Each inner list is the properties/columns # involved in the constraint sub unique_property_sets { my $self = shift; if ($self->{_unique_property_sets}) { return @{ $self->{_unique_property_sets} }; } my $unique_property_sets = $self->{_unique_property_sets} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{constraints}) { for my $spec (@{ $class_meta->{constraints} }) { push @$unique_property_sets, [ @{ $spec->{properties} } ] } } } return @$unique_property_sets; } # Return the constraint information as a hashref # keys are the SQL constraint name, values are a listref of property/column names involved sub unique_property_set_hashref { my $self = shift; if ($self->{_unique_property_set_hashref}) { return $self->{_unique_property_set_hashref}; } my $unique_property_set_hashref = $self->{_unique_property_set_hashref} = {}; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{'constraints'}) { for my $spec (@{ $class_meta->{'constraints'} }) { my $unique_group = $spec->{'sql'}; next if ($unique_property_set_hashref->{$unique_group}); # child classes override parents $unique_property_set_hashref->{$unique_group} = [ @{$spec->{properties}} ]; } } } return $unique_property_set_hashref; } # Used by the class meta meta data constructors to make changes in the # raw data stored in the class object's hash. These should really # only matter while running ur update # Args are: # 1) An UR::Object::Property object with attribute_name, class_name, id, property_name, type_name # 2) The method called: _construct_object, load, # 3) An id? sub _property_change_callback { my($property_obj,$method, $old_val, $new_val) = @_; return if ($method eq 'load' || $method eq 'unload'); return unless ref($property_obj); # happens when, say, error_message is called on the UR::Object::Property class my $class_obj = UR::Object::Type->get(class_name => $property_obj->class_name); my $property_name = $property_obj->property_name; $old_val = '' unless(defined $old_val); $new_val = '' unless(defined $new_val); if ($method eq 'create') { unless ($class_obj->{'has'}->{$property_name}) { my @attr = qw( class_name data_length data_type is_delegated is_optional property_name ); my %new_property; foreach my $attr_name (@attr ) { $new_property{$attr_name} = $property_obj->$attr_name(); } $class_obj->{'has'}->{$property_name} = \%new_property; } if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'create'); } } elsif ($method eq 'delete') { if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'delete'); } delete $class_obj->{'has'}->{$property_name}; } elsif ($method eq 'is_id' and $new_val ne $old_val) { my $change = $new_val ? 'create' : 'delete'; &_id_property_change_callback($property_obj, $change); } if (exists $class_obj->{'has'}->{$property_name} && exists $class_obj->{'has'}->{$property_name}->{$method}) { $class_obj->{'has'}->{$property_name}->{$method} = $new_val; } # Invalidate the cache used by all_property_names() for my $key (@cache_keys) { $class_obj->_invalidate_cached_data_for_subclasses($key); } } # Some expensive-to-calculate data gets stored in the class meta hashref # and needs to be removed for all the existing subclasses sub _invalidate_cached_data_for_subclasses { my($class_meta, @cache_keys) = @_; delete @$class_meta{@cache_keys}; my @subclasses = @{$UR::Object::Type::_init_subclasses_loaded{$class_meta->class_name}}; my %seen; while (my $subclass = shift @subclasses) { next if ($seen{$subclass}++); my $sub_meta = UR::Object::Type->get(class_name => $subclass); delete @$sub_meta{@cache_keys}; push @subclasses, @{$UR::Object::Type::_init_subclasses_loaded{$sub_meta->class_name}}; } } # A streamlined version of the method just below that dosen't check that the # data in both places is the same before a delete operation. What was happening # was that an ID property got deleted and the position checks out ok, but then # a second ID property gets deleted and now the position dosen't match because we # aren't able to update the object's position property 'cause it's an ID property # and can't be changed. # # The short story is that we've lowered the bar for making sure it's safe to delete info sub _id_property_change_callback { my $property_obj = shift; my $method = shift; return if ($method eq 'load' || $method eq 'unload'); my $class = UR::Object::Type->get(class_name => $property_obj->class_name); if ($method eq 'create') { my $pos = $property_obj->id_by; $pos += 0; # make sure it's a number if ($pos <= @{$class->{'id_by'}}) { splice(@{$class->{'id_by'}}, $pos, 0, $property_obj->property_name); } else { # $pos is past the end... probably an id property was deleted and another added push @{$class->{'id_by'}}, $property_obj->property_name; } } elsif ($method eq 'delete') { my $property_name = $property_obj->property_name; for (my $i = 0; $i < @{$class->{'id_by'}}; $i++) { if ($class->{'id_by'}->[$i] eq $property_name) { splice(@{$class->{'id_by'}}, $i, 1); return; } } #$DB::single = 1; Carp::confess("Internal data consistancy problem: could not find property named $property_name in id_by list for class meta " . $class->class_name); } else { # Shouldn't get here since ID properties can't be changed, right? #$DB::single = 1; Carp::confess("Shouldn't be here as ID properties can't change"); 1; } $class->{'_all_id_property_names'} = undef; # Invalidate the cache used by all_id_property_names } # # BOOTSTRAP CODE # sub get_with_special_parameters { my $class = shift; my $rule = shift; my %extra = @_; if (my $namespace = delete $extra{'namespace'}) { unless (keys %extra) { my @c = $namespace->get_material_classes(); @c = grep { $_->namespace eq $namespace } $class->is_loaded($rule->params_list); return $class->context_return(@c); } } return $class->SUPER::get_with_special_parameters($rule,@_); } sub __signal_change__ { my $self = shift; my @rv = $self->SUPER::__signal_change__(@_); if ($_[0] eq "delete") { my $class_name = $self->{class_name}; $self->ungenerate(); } return @rv; } my @default_valid_signals = qw(create delete commit rollback load unload load_external subclass_loaded); our %STANDARD_VALID_SIGNALS; @STANDARD_VALID_SIGNALS{@default_valid_signals} = (1) x @default_valid_signals; sub _is_valid_signal { my $self = shift; my $aspect = shift; # An aspect of empty string (or undef) means all aspects are being observed. return 1 unless (defined($aspect) and length($aspect)); # All standard creation and destruction methods emit a signal. return 1 if ($STANDARD_VALID_SIGNALS{$aspect}); for my $property ($self->all_property_names) { return 1 if $property eq $aspect; } if (!exists $self->{'_is_valid_signal'}) { $self->{'_is_valid_signal'} = { map { $_ => 1 } @{$self->{'valid_signals'}} }; } return 1 if ($self->{'_is_valid_signal'}->{$aspect}); foreach my $parent_meta ( $self->parent_class_metas ) { if ($parent_meta->_is_valid_signal($aspect)) { $self->{'_is_valid_signal'}->{$aspect} = 1; return 1; } } return 0; } sub generated { my $self = shift; if (@_) { $self->{'generated'} = shift; } return $self->{'generated'}; } sub ungenerate { my $self = shift; my $class_name = $self->class_name; delete $UR::Object::_init_subclass->{$class_name}; delete $UR::Object::Type::_inform_all_parent_classes_of_newly_loaded_subclass{$class_name}; do { no strict; no warnings; my @symbols_which_are_not_subordinate_namespaces = grep { substr($_,-2) ne '::' } keys %{ $class_name . "::" }; my $hr = \%{ $class_name . "::" }; delete @$hr{@symbols_which_are_not_subordinate_namespaces}; }; my $module_name = $class_name; $module_name =~ s/::/\//g; $module_name .= ".pm"; delete $INC{$module_name}; $self->{'generated'} = 0; } sub singular_accessor_name_for_is_many_accessor { my($self, $property_name) = @_; unless (exists $self->{_accessor_singular_names}->{$property_name}) { my $property_meta = $self->property_meta_for_name($property_name) if ($self->generated); if ($bootstrapping # trust the caller when bootstrapping or ! $self->generated # when called from UR::Object::Type::AccessorWriter and the property isn't created yet or ($property_meta && $property_meta->is_many) ) { require Lingua::EN::Inflect; $self->{_accessor_singular_names}->{$property_name} = Lingua::EN::Inflect::PL_V($property_name); } else { $self->{_accessor_singular_names}->{$property_name} = undef; } } return $self->{_accessor_singular_names}->{$property_name}; } sub iterator_accessor_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "${singular}_iterator"; } sub set_accessor_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "${singular}_set"; } sub rule_accessor_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "__${singular}_rule"; } sub arrayref_accessor_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "${singular}_arrayref"; } sub adder_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "add_${singular}"; } sub remover_name_for_is_many_accessor { my($self, $property_name) = @_; my $singular = $self->singular_accessor_name_for_is_many_accessor($property_name); return $singular && "remove_${singular}"; } 1; ModuleWriter.pm100664023532023421 7477412544604516 20103 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Typepackage UR::Object::Type::ModuleWriter; # to help the installer package UR::Object::Type; # hold methods for the class which cover Module Read/Write. use strict; use warnings; require UR; use List::MoreUtils qw(first_index); use Scalar::Util qw(looks_like_number); our $VERSION = "0.44"; # UR $VERSION; our %meta_classes; our $bootstrapping = 1; our @partially_defined_classes; our $pwd_at_compile_time = cwd(); sub resolve_class_description_perl { my $self = $_[0]; no strict 'refs'; my @isa = @{ $self->class_name . "::ISA" }; use strict; unless (@isa) { my @i = ${ $self->is }; my @parent_class_objects = map { UR::Object::Type->is_loaded(class_name => $_) } @i; die "Parent class objects not all loaded for " . $self->class_name unless (@i == @parent_class_objects); @isa = map { $_->class_name } @parent_class_objects; } unless (@isa) { my @i = ${ $self->is }; my @parent_class_objects = map { UR::Object::Type->is_loaded(class_name => $_) } @i; unless (@i and @i == @parent_class_objects) { Carp::confess("No inheritance meta-data found for ( @i / @parent_class_objects)" . $self->class_name) } @isa = map { $_->class_name } @parent_class_objects; } my $class_name = $self->class_name; my @parent_classes = $self->parent_class_metas; my $has_table = $self->has_table; # For getting default values for some of the properties my $class_meta_meta = UR::Object::Type->get(class_name => 'UR::Object::Type'); my $perl = ''; unless (@isa == 1 and $isa[0] =~ /^UR::Object|UR::Entity$/ ) { $perl .= " is => " . (@isa == 1 ? "'@isa',\n" : pprint_arrayref(\@isa) . ",\n"); } $perl .= " table_name => " . ($self->table_name ? "'" . $self->table_name . "'" : 'undef') . ",\n" if $self->data_source_id; $perl .= " is_abstract => 1,\n" if $self->is_abstract; $perl .= " er_role => '" . $self->er_role . "',\n" if ($self->er_role and ($self->er_role ne $class_meta_meta->property_meta_for_name('er_role')->default_value)); if ($self->{type_has}) { my @keys = qw(is_optional is); my %type_has = @{$self->{type_has}}; my @type_has_names = keys %type_has; my $section_src; for my $name (@type_has_names) { my $struct = $type_has{$name}; $section_src .= pprint_subsection($name, _section_lines($struct, @keys)); } if ($section_src) { $perl .= pprint_section('type_has', $section_src); } } # Meta-property attributes my @property_meta_property_names; if ($self->{'attributes_have'}) { @property_meta_property_names = sort { $self->{'attributes_have'}->{$a}->{'position_in_module_header'} <=> $self->{'attributes_have'}->{$b}->{'position_in_module_header'} } keys %{$self->{'attributes_have'}}; my $section_src = ''; foreach my $meta_name ( @property_meta_property_names ) { my $this_meta_struct = $self->{'attributes_have'}->{$meta_name}; # The attributes_have structure gets propogated to subclasses, but it only needs to appear # in the class definition of the most-parent class my $expected_name = $class_name . '::attributes_have'; next unless ( $this_meta_struct->{'is_specified_in_module_header'} eq $expected_name); # We want these to appear first my @this_meta_properties; # skip the ones we've already done my @exclude_keys = qw(is_specified_in_module_header position_in_module_header); my @keys = _exclude_items([keys %$this_meta_struct], \@exclude_keys); $section_src .= pprint_subsection($meta_name, _section_lines($this_meta_struct, @keys)); } if ($section_src) { $perl .= pprint_section('attributes_have', $section_src); } } if (exists $self->{'first_sub_classification_method_name'}) { # This gets overridden by UR::Object::Type to cache the value it finds from parent # classes in __first_sub_classification_method_name, so we can't just get the # property through the normal channels $perl .= " first_sub_classification_method_name => '" . $self->{'first_sub_classification_method_name'} ."',\n"; } # These property names are either written in other places in this sub, or shouldn't be written out my %addl_property_names = map { $_ => 1 } $self->__meta__->all_property_type_names; my @specified = qw/is class_name table_name id_by er_role is_abstract generated data_source_id schema_name doc namespace id first_sub_classification_method_name property_metas pproperty_names id_property_metas meta_class_name id_generator valid_signals/; delete @addl_property_names{@specified}; for my $property_name (sort keys %addl_property_names) { my $property_obj = $self->__meta__->property_meta_for_name($property_name); next if ($property_obj->is_calculated or $property_obj->is_delegated); my $property_value = $self->$property_name; my $default_value = $property_obj && $property_obj->default_value; # If the property is set on the class object # and both the value and default are numeric and numerically different, # or stringly different than the default no warnings qw( numeric uninitialized ); if ( defined $property_value and ( ($property_value + 0 eq $property_value and $default_value + 0 eq $default_value and $property_value != $default_value) or ($property_value ne $default_value) ) ) { # then it should show up in the class definition my $value = $self->$property_name; if (ref $value eq 'ARRAY') { $value = pprint_arrayref($value); } else { $value = qq('$value'); } $perl .= " $property_name => $value,\n"; } } my %properties_by_section; my %id_property_names = map { $_ => 1 } $self->direct_id_property_names; my @properties = $self->direct_property_metas; foreach my $property_meta ( @properties ) { my $mentioned_section = $property_meta->is_specified_in_module_header; next unless $mentioned_section; # skip implied properites ($mentioned_section) = ($mentioned_section =~ m/::(\w+)$/); if (($mentioned_section and $mentioned_section eq 'id_implied') or $id_property_names{$property_meta->property_name}) { push @{$properties_by_section{'id_by'}}, $property_meta; } elsif ($mentioned_section) { push @{$properties_by_section{$mentioned_section}}, $property_meta; } else { push @{$properties_by_section{'has'}}, $property_meta; } } my %sections_seen; my $data_source_id = $self->data_source_id; my ($data_source) = ($data_source_id ? UR::DataSource->get($data_source_id) : undef); foreach my $section ( ( 'id_by', 'has', 'has_many', 'has_optional', keys(%properties_by_section) ) ) { next unless ($properties_by_section{$section}); next if ($sections_seen{$section}); $sections_seen{$section} = 1; # New properites (will have position_in_module_header == undef) should go at the end my @properties = sort { my $pos_a = defined($a->{'position_in_module_header'}) ? $a->{'position_in_module_header'} : 1000000; my $pos_b = defined($b->{'position_in_module_header'}) ? $b->{'position_in_module_header'} : 1000000; $pos_a <=> $pos_b; } @{$properties_by_section{$section}}; my $section_src = ''; foreach my $property_meta ( @properties ) { my $name = $property_meta->property_name; my @fields = $self->_get_display_fields_for_property( $property_meta, has_table => $has_table, section => $section, data_source => $data_source, attributes_have => \@property_meta_property_names); $section_src .= pprint_subsection($name, @fields); } $perl .= pprint_section($section, $section_src); } my $unique_groups = $self->unique_property_set_hashref; if ($unique_groups and keys %$unique_groups) { $perl .= " unique_constraints => [\n"; for my $unique_group_name (keys %$unique_groups) { my $property_names = join(' ', sort { $a cmp $b } @{ $unique_groups->{$unique_group_name}}); $perl .= " { " . "properties => [qw/$property_names/], " . "sql => '" . $unique_group_name . "'" . " },\n"; } $perl .= " ],\n"; } $perl .= " schema_name => '" . $self->schema_name . "',\n" if $self->schema_name; $perl .= " data_source => '" . $self->data_source_id . "',\n" if $self->data_source_id; my $print_id_generator; if (my $id_generator = $self->id_generator) { if ($self->data_source_id and $id_generator eq '-urinternal') { $print_id_generator = 1; } elsif (! $self->data_source_id and $id_generator eq '-urinternal') { $print_id_generator = 0; } else { $print_id_generator = 1; } $perl .= " id_generator => '$id_generator',\n" if ($print_id_generator); } if (my $valid_signals = $self->valid_signals) { if (ref($valid_signals) ne 'ARRAY') { Carp::croak("The 'valid_signals' metadata for class $class_name must be an arrayref, got: ".Data::Dumper::Dumper($valid_signals)); } elsif (@$valid_signals) { $perl .= " valid_signals => ['" . join("', '", @$valid_signals) . "'],\n"; } } my $doc = $self->doc; if (defined($doc)) { $doc = Dumper($doc); $doc =~ s/\$VAR1 = //; $doc =~ s/;\s*$//; } $perl .= " doc => $doc,\n" if defined($doc); return $perl; } sub resolve_module_header_source { my $self = shift; my $class_name = $self->class_name; my $perl = "class $class_name {\n"; $perl .= $self->resolve_class_description_perl; $perl .= "};\n"; return $perl; } my $next_line_prefix = "\n"; my $deep_indent_prefix = "\n" . (" " x 55); sub _get_display_fields_for_property { my $self = shift; my $property = shift; my %params = @_; if (not $property->is_specified_in_module_header) { # we omit showing implied properties which have no additional data, # unless they have their own docs, a specified column, etc. return(); } my @fields; my $property_name = $property->property_name; my $type = $property->data_type; if ($type) { push @fields, "is => '$type'" if $type; } if (defined($property->data_length) and length($property->data_length)) { push @fields, "len => " . $property->data_length; } if ($property->is_legacy_eav) { # temp hack for entity attribute values push @fields, "is_legacy_eav => 1"; } elsif ($property->is_delegated) { # do nothing } elsif ($property->is_calculated) { if (my $calc_from = $property->calculate_from) { if ($calc_from and @$calc_from == 1) { push @fields, "calculate_from => '" . $calc_from->[0] . "'"; } elsif ($calc_from) { push @fields, "calculate_from => [ '" . join("', '", @$calc_from) . "' ]"; } } my $calc_source; foreach my $calc_type ( qw( calculate calculate_sql calculate_perl calculate_js ) ) { if ($property->$calc_type) { $calc_source = 1; push @fields, "$calc_type => q(" . $property->$calc_type . ")"; } } push @fields, 'is_calculated => 1' unless ($calc_source); } elsif ($params{has_table} && ! $property->is_transient) { unless ($property->column_name) { die("no column for property on class with table: " . $property->property_name . " class: " . $self->class_name . "?"); } my $ds = $params{'data_source'}; my $should_uc = ($ds && $ds->table_and_column_names_are_upper_case); my $cname = $property->column_name; my $pname = $property->property_name; my $expected_cname = $should_uc ? uc($pname) : $pname; if ($cname ne $expected_cname) { push @fields, "column_name => '" . $cname . "'"; } } if (defined($property->default_value)) { my $value = $property->default_value; if (! looks_like_number($value)) { $value = "'$value'"; } push @fields, "default_value => $value"; } if (my @id_by = eval { $property->get_property_name_pairs_for_join }) { unless (defined $property->reverse_as) { push @fields, "id_by => " . (@id_by > 1 ? '[ ' : '') . join(", ", map { "'" . $_->[0] . "'" } @id_by) . (@id_by > 1 ? ' ]' : ''); } if (defined $property->id_class_by) { push @fields, sprintf("id_class_by => '%s'", $property->id_class_by); } } if ($property->via) { push @fields, "via => '" . $property->via . "'"; if ($property->to and $property->to ne $property->property_name) { push @fields, "to => '" . $property->to . "'"; } if ($property->is_mutable) { # via properties are not usually mutable push @fields, 'is_mutable => 1'; } my $via_property_name = $property->via; if ($via_property_name eq '__self__') { $via_property_name = $property->to; } my $via = $property->class_name->__meta__->properties(property_name => $via_property_name); if (! $via) { # maybe via a method?? Safer to use is_many than not push @fields, 'is_many => 1'; } elsif ($property->is_many ne $via->is_many) { push @fields, 'is_many => ' . $property->is_many; } } if ($property->reverse_as) { push @fields, "reverse_as => '" . $property->reverse_as . "'"; if ($property->is_mutable) { # reverse_as properties are not usually mutable push @fields, 'is_mutable => 1'; } } if ($property->constraint_name) { push @fields, "constraint_name => '" . $property->constraint_name . "'"; } if ($property->where) { my @where_parts = (); my @where = @{ $property->where }; while (@where) { my $prop_name = shift @where; my $comparison = shift @where; # wrap 'property operator' with quotes if it contains space if (index($prop_name, ' ') >= 0) { $prop_name = "'$prop_name'"; } if (! ref($comparison)) { # It's a strictly equals comparison. # wrap it in quotes... $comparison = "'$comparison'"; } elsif (ref($comparison) eq 'HASH') { # It's a more complicated operator my @operator_parts = (); foreach my $key ( 'operator', 'value', keys %$comparison ) { if ($comparison->{$key}) { if (ref($comparison->{$key})) { my $class_name = $property->class_name; Carp::croak("Modulewriter doesn't know how to handle property $property_name of class $class_name. Its 'where' has a non-scalar value for the '$key' key"); } push @operator_parts, "$key => '" . delete($comparison->{$key}) . "'"; } } $comparison = '{ ' . join(', ', @operator_parts) . ' } '; } elsif (ref($comparison) eq 'ARRAY') { $comparison = pprint_arrayref($comparison); } else { my $class_name = $property->class_name; Carp::croak("Modulewriter doesn't know how to handle property $property_name of class $class_name. Its 'where' is not a simple scalar, hashref, or arrayref."); } push @where_parts, "$prop_name => $comparison"; } push @fields, 'where => [ ' . join(', ', @where_parts) . ' ]'; } if (my $valid_values_arrayref = $property->valid_values) { push @fields, "valid_values => " . pprint_arrayref($valid_values_arrayref); } if (my $example_values_arrayref = $property->example_values) { push @fields, "example_values => " . pprint_arrayref($example_values_arrayref); } # All the things like is_optional, is_many, etc # show only true values, false is default # section can be things like 'has', 'has_optional' or 'has_transient_many_optional' my $section = $params{'section'}; $section =~ m/^has_(.*)/; my @sections = split('_',$1 || ''); for my $std_field_name (qw/optional abstract transient constant classwide many deprecated/) { next if (grep { $std_field_name eq $_ } @sections); # Don't print is_optional if we're in the has_optional section my $property_name = "is_" . $std_field_name; push @fields, "$property_name => " . $property->$property_name if $property->$property_name; } foreach my $meta_property ( @{$params{'attributes_have'}} ) { my $value = $property->{$meta_property}; if (defined $value) { my $format = looks_like_number($value) ? "%s => %s" : "%s => '%s'"; push @fields, sprintf($format, $meta_property, $value); } } my $desc = $property->doc; if ($desc && length($desc)) { $desc =~ s/([\$\@\%\\\"])/\\$1/g; $desc =~ s/\n/\\n/g; if ($desc =~ /'/) { $desc = "q($desc)"; } else { $desc = "'$desc'"; } push @fields, $next_line_prefix . "doc => $desc"; } return @fields; } sub module_base_name { my $file_name = shift->class_name; $file_name =~ s/::/\//g; $file_name .= ".pm"; return $file_name; } sub module_path { my $self = shift; my $base_name = $self->module_base_name; my $path = $INC{$base_name}; return _abs_path_relative_to_pwd_at_compile_time($path) if $path; my $namespace; my $first_slash = index($base_name, '/'); if ($first_slash >= 0) { # Normal case... $namespace = substr($base_name, 0, $first_slash); $namespace .= ".pm"; } else { # This module must _be_ the namespace $namespace = $base_name; } for my $dir (map { _abs_path_relative_to_pwd_at_compile_time($_) } grep { -d $_ } @INC) { if (-e $dir . "/" . $namespace) { my $try_path = $dir . '/' . $base_name; return $try_path; } } return; } sub _abs_path_relative_to_pwd_at_compile_time { # not a method my $path = shift; if ($path !~ /^[\/\\]/) { $path = $pwd_at_compile_time . '/' . $path; } my $path2 = Cwd::abs_path($path); return $path2; } sub module_directory { my $self = shift; my $base_name = $self->module_base_name; my $path = $self->module_path; return unless defined($path) and length($path); unless ($path =~ s/$base_name$//) { Carp::confess("Failed to find base name $base_name at the end of path $path!") } return $path; } sub module_data_subdirectory { my $self = shift; my $path = $self->module_path; $path =~ s/.pm$//; return $path; } sub module_source_lines { my $self = shift; my $file_name = $self->module_path; my $in = IO::File->new("<$file_name"); unless ($in) { return (undef,undef,undef); } my @module_src = <$in>; $in->close; return @module_src } sub module_source { join("",shift->module_source_lines); } sub module_header_positions { my $self = shift; my @module_src = $self->module_source_lines; my $namespace = $self->namespace; my $class_name = $self->class_name; unless ($self->namespace) { die "No namespace on $self->{class_name}?" } $namespace = 'UR' if $namespace eq $self->class_name; my $state = 'before'; my ($begin,$end,$use); for (my $n = 0; $n < @module_src; $n++) { my $line = $module_src[$n]; if ($state eq 'before') { if ($line and $line =~ /^use $namespace;/) { $use = $n; } if ( $line and ( $line =~ /^define UR::Object::Type$/ or $line =~ /^(App|UR)::Object::(Type|Class)->(define|create)\($/ or $line =~ /^class\s*$class_name\b/ ) ) { $begin = $n; $state = 'during'; } else { } } elsif ($state eq 'during') { my $old_meta_src .= $line; # FIXME this dosen't appear anywhere else?? if ($line =~ /^(\)|\}|);\s*$/) { $end = $n; $state = 'after'; } } } # cache $self->{module_header_positions} = [$begin,$end,$use]; # return return ($begin,$end,$use); } sub module_header_source_lines { my $self = shift; my ($begin,$end,$use) = $self->module_header_positions; my @src = $self->module_source_lines; return unless $end; @src[$begin..$end]; } sub module_header_source { join("",shift->module_header_source_lines); } sub rewrite_module_header { use strict; use warnings; my $self = shift; my $package = $self->class_name; # generate new class metadata my $new_meta_src = $self->resolve_module_header_source; unless ($new_meta_src) { die "Failed to generate source code for $package!"; } my ($begin,$end,$use) = $self->module_header_positions; my $namespace = $self->namespace; $namespace = 'UR' if $namespace eq $self->class_name; unless ($namespace) { ($namespace) = ($package =~ /^(.*?)::/); } $new_meta_src = "use $namespace;\n" . $new_meta_src unless $use; # determine the path to the module # this may not exist my $module_file_path = $self->module_path; # temp safety hack if ($module_file_path =~ "/gsc/scripts/lib") { Carp::confess("attempt to write directly to the app server!"); } # determine the new source for the module my @module_src; my $old_file_data; if (-e $module_file_path) { # rewrite the existing module # find the old positions of the module header @module_src = $self->module_source_lines; # cleanup legacy cruft unless ($namespace eq 'UR') { @module_src = map { ($_ =~ m/^use UR;/?"":$_) } @module_src; } if (!grep {$_ =~ m/^use warnings;/} @module_src) { $new_meta_src = "use warnings;\n" . $new_meta_src; } if (!grep {$_ =~ m/^use strict;/} @module_src) { $new_meta_src = "use strict;\n" . $new_meta_src; } # If $begin and $end are undef, then module_header_positions() didn't find any # old code and we're inserting all brand new stuff. Look for the package declaration # and insert after that. my $len; if (defined $begin || defined $end) { $len = $end-$begin+1; } else { # is there a more fool-proof way to find it? for ($begin = 0; $begin < $#module_src; ) { last if ($module_src[$begin++] =~ m/package\s+$package;/); } $len = 0; } # replace the old lines with the new source # note that the inserted "row" is multi-line, but joins nicely below... splice(@module_src,$begin,$len,$new_meta_src); my $f = IO::File->new($module_file_path); $old_file_data = join('',$f->getlines); $f->close(); } else { # write new module source # put =cut marks around it if this is a special metadata class # the definition at the top is non-functional for bootstrapping reasons if ($meta_classes{$package}) { $new_meta_src = "=cut\n\n$new_meta_src\n\n=cut\n\n"; $self->warning_message("Meta package $package"); } @module_src = join("\n", "package " . $self->class_name . ";", "", "use strict;", "use warnings;", "", $new_meta_src, "1;", "" ); } $ENV{'HOST'} ||= ''; my $temp = "$module_file_path.$$.$ENV{HOST}"; my $temp_dir = $module_file_path; $temp_dir =~ s/\/[^\/]+$//; unless (-d $temp_dir) { print "mkdir -p $temp_dir\n"; system "mkdir -p $temp_dir"; } my $out = IO::File->new(">$temp"); unless ($out) { die "Failed to create temp file $temp!"; } for (@module_src) { $out->print($_) }; $out->close; my $rv = system qq(perl -e 'eval `cat $temp`' 2>/dev/null 1>/dev/null); $rv /= 255; if ($rv) { die "Module is not compilable with new source!"; } else { unless (rename $temp, $module_file_path) { die "Error renaming $temp to $module_file_path!"; } } UR::Context::Transaction->log_change($self, ref($self), $self->id, 'rewrite_module_header', Data::Dumper::Dumper{path => $module_file_path, data => $old_file_data}); return 1; } sub pprint_arrayref { my $arrayref = shift; # Useqq(1) causes newlines to be escaped so the only newlines are those # injected by Indent(1). Useqq(1) also quotes string values so we can # strip the whitespace around the newlines. my $value_string = Data::Dumper->new([$arrayref])->Terse(1)->Indent(1)->Useqq(1)->Dump; $value_string =~ s/\s*\n\s*/ /g; $value_string =~ s/\s*$//; return $value_string; } sub pprint_section { my ($section, $section_src) = @_; my $indent_section = ' ' x 4; return "$indent_section$section => [\n$section_src$indent_section],\n"; } { my $indent_name = ' ' x 8; my $indent_key = $indent_name . ' ' x 4; my $max_width = 78; sub pprint_subsection { my ($name, @fields) = @_; foreach ( @fields ) { s/^\s+// } my $section_src = _pprint_subsection_one_line($name, @fields); if (length($section_src) > $max_width) { $section_src = _pprint_subsection_multi_line($name, @fields); } return $section_src; } sub _pprint_subsection_one_line { my $name = shift; return $indent_name . $name . ' => { ' . join(', ', @_) . " },\n"; } sub _pprint_subsection_multi_line { my $name = shift; return $indent_name . $name . " => {\n" . $indent_key . join(",\n$indent_key", @_) . ",\n" . $indent_name . "},\n"; } } sub _quoted_value { my $value = shift; my ($qo, $qc) = ('', ''); if (!looks_like_number($value)) { if ($value =~ /'/) { ($qo, $qc) = ('q(', ')'); } else { ($qo, $qc) = ("'", "'"); } } return "$qo$value$qc"; } sub _idx { my $e = shift; my @expected_order = qw( is is_optional ); my $e_idx = first_index { $_ eq $e } @expected_order; if ($e_idx == -1) { $e_idx = scalar(@expected_order); } return $e_idx; } sub _key_sorter { my ($a_idx, $b_idx) = (_idx($a), _idx($b)); my $cmp; if ($a_idx == $b_idx) { $cmp = $a cmp $b; } else { $cmp = $a_idx <=> $b_idx; } return $cmp; } sub _sort_keys { sort _key_sorter @_; } sub _exclude_items { my ($list, $exclude) = @_; return grep { my $l = $_; !grep { my $e = $_; $e eq $l; } @$exclude; } @$list; } sub _section_lines { my ($struct, @keys) = @_; @keys = _sort_keys(@keys); my @lines = map { my $value = _quoted_value($struct->{$_}); sprintf('%s => %s', $_, $value); } @keys; return @lines; } 1; =pod =head1 NAME UR::Object::Type::ModuleWriter - Helper module for UR::Object::Type responsible for writing Perl modules =head1 DESCRIPTION Subroutines within this module actually live in the UR::Object::Type namespace; this module is just a convienent place to collect them. The Module Writer is used by the class updater system (L<(UR::Namespace::Command::Update::Classes> and 'ur update classes) to add, remove and alter the Perl modules behind the classes within a Namespace. =head1 METHODS =over 4 =item resolve_module_header_source $classobj->resolve_module_header_source(); Returns a string that represents a fully-formed class definition the the given class metaobject $classobj. =item resolve_class_description_perl $classobj->resolve_class_description_perl() Used by resolve_module_header_source(). This method inspects all the applicable properties of the class metaobject and builds up a string that gets inserted between the {...} of the class definition string. =item rewrite_module_header $classobj->rewrite_module_header(); This method rewrites an existing Perl module file in place for the class metaobject, or creates a new file if one does not already exist. =item module_base_name Returns the pathname of the class's module relative to the top level directory of that class's Namespace. =item module_path Returns the fully qualified pathname of the class's module. =item module_source_lines Returns the text of the class's Perl module as a list of strings. =item module_source Returns the text of the class's Perl module as a single string. =item module_header_positions Returns a 3-element list ($begin, $end, $use) where $begin is the line number where the class header begins. $end is the line number where it ends. $use is the line number where the module declares that it use's a Namespace. =item module_header_source_lines Returns the text of the class's Perl module source where the class definition is as a list of strings. =item module_header_source Returns the text of the class's Perl module source where the class definition is as a single string. =back =head1 SEE ALSO UR::Object::Type, UR::Object::Type::Initializer =cut Json.pm100664023532023421 122112544604516 22133 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/View/AvailableViewspackage UR::Object::Type::View::AvailableViews::Json; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use UR::Object::Type::View::AvailableViews::Xml; class UR::Object::Type::View::AvailableViews::Json { is => 'UR::Object::View::Default::Json', has_constant => [ perspective => { value => 'available-views' }, ], }; sub _jsobj { my $self = shift; my $subject = $self->subject; return unless $subject; my $target_class = $subject->class_name; my %perspectives = UR::Object::Type::View::AvailableViews::Xml::_find_perspectives($self, $target_class); return \%perspectives; } 1; Xml.pm100664023532023421 504512544604516 21772 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/View/AvailableViewspackage UR::Object::Type::View::AvailableViews::Xml; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Type::View::AvailableViews::Xml { is => 'UR::Object::View::Default::Xml', has_constant => [ perspective => { value => 'available-views' }, ], }; sub _generate_content { my $self = shift; my $subject = $self->subject; return unless $subject; my $xml_doc = XML::LibXML->createDocument(); $self->_xml_doc($xml_doc); my $target_class = $subject->class_name; my %perspectives = $self->_find_perspectives($target_class); my $perspectives = $xml_doc->createElement('perspectives'); $xml_doc->setDocumentElement($perspectives); for my $key (sort keys %perspectives) { my $perspective = $perspectives->addChild( $xml_doc->createElement('perspective') ); $perspective->addChild( $xml_doc->createAttribute('name', $key) ); for my $tool_key (sort keys %{$perspectives{$key}}) { my $toolkit = $perspective->addChild( $xml_doc->createElement('toolkit')); $toolkit->addChild( $xml_doc->createAttribute('name', $tool_key)); } } $perspectives->addChild( $xml_doc->createAttribute( 'type', $target_class )); return $xml_doc->toString(1); } sub _find_perspectives { my $self = shift; my $target_class = shift; my %perspectives; for my $class ($target_class, $target_class->inheritance) { next unless $class->isa('UR::Object'); my $namespace = $class->__meta__->namespace; my $dir = $class; $dir =~ s!::!/!g; $dir =~ s!^$namespace/!!; $dir .= '/View'; my @views = $namespace->_get_class_names_under_namespace($dir); for my $view (@views) { if(my $view_type = UR::Object::Type->get($view)) { next unless $view->isa('UR::Object::View'); my $perspective = $view_type->property_meta_for_name('perspective')->default_value; my $toolkit = $view_type->property_meta_for_name('toolkit')->default_value; unless($perspective) { $self->error_message('No perspective set on view class: ' . $view_type->class_name); next; } unless($toolkit) { $self->error_message('No toolkit set on view class: ' . $view_type->class_name); next; } $perspectives{$perspective}{$toolkit}++; } } } return %perspectives; } 1; Text.pm100664023532023421 114212544604516 20636 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/View/Defaultpackage UR::Object::Type::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['is','direct_property_names'], }, ], ); 1; =pod =head1 NAME UR::Object::Type::View::Default::Text - View class for class metaobjects =head1 DESCRIPTION This class is used by L and L to construct the text outputted. =cut Xml.pm100664023532023421 360412544604516 20457 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Type/View/Defaultpackage UR::Object::Type::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Xml', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => [ 'namespace', 'table_name', 'data_source_id', 'is_abstract', 'is_final', 'is_singleton', 'is_transactional', 'schema_name', 'meta_class_name', 'first_sub_classification_method_name', 'sub_classification_method_name', { label => 'Properties', name => 'properties', subject_class_name => 'UR::Object::Property', perspective => 'default', toolkit => 'xml', aspects => [ 'is_id', 'property_name', 'column_name', 'data_type', 'is_optional' ], }, { label => 'References', name => 'all_id_by_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'default', toolkit => 'xml', aspects => [], } ], }, ], ); 1; =pod =head1 NAME UR::Object::Type::View::Default::Xml - View class for class metaobjects =head1 DESCRIPTION This class is used by L to build an html representation. =cut Value.pm100664023532023421 137312544604516 15555 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::Value; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::Value { is => 'UR::Value', is_abstract => 1, subclassify_by => 'entity_class_name', type_has => [ entity_class_name => { is => 'Text' }, ], has => [ rule => { is => 'UR::BoolExpr', id_by => 'id' }, entity_class_name => { via => 'rule', to => 'subject_class_name' }, ], doc => 'an unordered group of distinct UR::Objects' }; sub AUTOSUB { my ($method,$class) = @_; my $entity_class_name = $class; $entity_class_name =~ s/::Value$//g; return unless $entity_class_name; my $code = $entity_class_name->can($method); return $code if $code; } 1; View.pm100664023532023421 5017012544604516 15432 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Objectpackage UR::Object::View; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; class UR::Object::View { has_abstract_constant => [ subject_class_name => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, perspective => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, toolkit => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, ], has_optional => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => 'when nested inside another view, this references that view', }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being observed' }, aspects => { is => 'UR::Object::View::Aspect', reverse_as => 'parent_view', is_many => 1, specify_by => 'name', order_by => 'number', doc => 'the aspects of the subject this view renders' }, default_aspects => { is => 'ARRAY', is_abstract => 1, is_constant => 1, is_many => 1, # technically this is one "ARRAY" default_value => undef, doc => 'a tree of default aspect descriptions' }, ], has_optional_transient => [ _widget => { doc => 'the object native to the specified toolkit which does the actual visualization' }, _observer_data => { is => 'HASH', is_transient => 1, value => undef, # hashref set at construction time doc => ' hooks around the subject which monitor it for changes' }, ], has_many_optional => [ aspect_names => { via => 'aspects', to => 'name' }, ] }; sub create { my $class = shift; my ($params,@extra) = $class->define_boolexpr(@_); # set values not specified in the params which can be inferred from the class name my ($expected_class,$expected_perspective,$expected_toolkit) = ($class =~ /^(.*)::View::(.*?)::([^\:]+)$/); unless ($params->specifies_value_for('subject_class_name')) { $params = $params->add_filter(subject_class_name => $expected_class); } unless ($params->specifies_value_for('perspective')) { $expected_perspective = join('-', split( /(?=[A-Z])/, $expected_perspective) ); #convert CamelCase to hyphenated-words $params = $params->add_filter(perspective => $expected_perspective); } unless ($params->specifies_value_for('toolkit')) { $params = $params->add_filter(toolkit => $expected_toolkit); } # now go the other way, and use both to infer a final class name $expected_class = $class->_resolve_view_class_for_params($params); unless ($expected_class) { my $subject_class_name = $params->value_for('subject_class_name'); Carp::croak("Failed to resolve a subclass of " . __PACKAGE__ . " for $subject_class_name from parameters. " . "Received $params."); } unless ($class->isa($expected_class)) { return $expected_class->create(@_); } $params->add_filter(_observer_data => {}); my $self = $expected_class->SUPER::create($params); return unless $self; $class = ref($self); $expected_class = $class->_resolve_view_class_for_params( subject_class_name => $self->subject_class_name, perspective => $self->perspective, toolkit => $self->toolkit ); unless ($expected_class and $expected_class eq $class) { $expected_class ||= ''; Carp::croak("constructed a $class object but properties indicate $expected_class should have been created."); } unless ($params->specifies_value_for('aspects')) { my @aspect_specs = $self->default_aspects(); if (! @aspect_specs) { @aspect_specs = $self->_resolve_default_aspects(); } if (@aspect_specs == 1 and ref($aspect_specs[0]) eq 'ARRAY') { # Got an arrayref, expand back into an array @aspect_specs = @{$aspect_specs[0]}; } for my $aspect_spec (@aspect_specs) { my $aspect = $self->add_aspect(ref($aspect_spec) ? %$aspect_spec : $aspect_spec); unless ($aspect) { $self->error_message("Failed to add aspect @$aspect_spec to new view " . $self->id); $self->delete; return; } } } return $self; } our %view_class_cache = (); sub _resolve_view_class_for_params { # View modules use standardized naming: SubjectClassName::View::Perspective::Toolkit. # The subject must be explicitly of class "SubjectClassName" or some subclass of it. my $class = shift; my $bx = $class->define_boolexpr(@_); if (exists $view_class_cache{$bx->id}) { if (!defined $view_class_cache{$bx->id}) { return; } return $view_class_cache{$bx->id}; } my %params = $bx->params_list; my $subject_class_name = delete $params{subject_class_name}; my $perspective = delete $params{perspective}; my $toolkit = delete $params{toolkit}; my $aspects = delete $params{aspects}; unless($subject_class_name and $perspective and $toolkit) { Carp::confess("Bad params @_. Expected subject_class_name, perspective, toolkit."); } $perspective = lc($perspective); $toolkit = lc($toolkit); my $namespace = $subject_class_name->__meta__->namespace; my $vocabulary = ($namespace and $namespace->can("get_vocabulary") ? $namespace->get_vocabulary() : undef); $vocabulary = UR->get_vocabulary; my $subject_class_object = $subject_class_name->__meta__; my @possible_subject_class_names = ($subject_class_name,$subject_class_name->inheritance); my $subclass_name; for my $possible_subject_class_name (@possible_subject_class_names) { $subclass_name = join("::", $possible_subject_class_name, "View", join ("", $vocabulary->convert_to_title_case ( map { ucfirst(lc($_)) } split(/-+|\s+/,$perspective) ) ), join ("", $vocabulary->convert_to_title_case ( map { ucfirst(lc($_)) } split(/-+|\s+/,$toolkit) ) ) ); my $subclass_meta; eval { $subclass_meta = $subclass_name->__meta__; }; if ($@ or not $subclass_meta) { #not a class... keep looking next; } unless($subclass_name->isa(__PACKAGE__)) { Carp::carp("Subclass $subclass_name exists but is not a view?!"); next; } $view_class_cache{$bx->id} = $subclass_name; return $subclass_name; } $view_class_cache{$bx->id} = undef; return; } sub _resolve_default_aspects { my $self = shift; my $parent_view = $self->parent_view; my $subject_class_name = $self->subject_class_name; my $meta = $subject_class_name->__meta__; my @c = ($meta->class_name, $meta->ancestry_class_names); my %aspects = map { $_->property_name => 1 } grep { not $_->implied_by } UR::Object::Property->get(class_name => \@c); my @aspects = sort keys %aspects; return @aspects; } sub __signal_change__ { # ensure that changes to the view which occur # after the widget is produced # are reflected in the widget my ($self,$method,@details) = @_; if ($self->_widget) { if ($method eq 'subject' or $method =~ 'aspects') { $self->_bind_subject(); } elsif ($method eq 'delete' or $method eq 'unload') { my $observer_data = $self->_observer_data; for my $subscription (values %$observer_data) { my ($class, $id, $method, $callback) = @$subscription; $class->cancel_change_subscription($id, $method, $callback); } $self->_widget(undef); } } return 1; } # _encompassing_view() and _subject_is_used_in_an_encompassing_view() are used by the # default views (UR::Object::View::Default::*) to detect an infinite recursion situation # where it's asked to render an object A that references a B which refers back to A # If this view is embedded in another view, return the encompassing view sub _encompassing_view { my $self = shift; my @aspects = UR::Object::View::Aspect->get(delegate_view_id => $self->id); if (@aspects) { # FIXME - is it possible for there to be more than one thing in @aspects here? # And if so, how do we differentiate them return $aspects[0]->parent_view; } # $self must be the top-level view return; } # If the subject of the view is also the subject of an encompassing view, return true sub _subject_is_used_in_an_encompassing_view { my($self,$subject) = @_; $subject = $self->subject unless (@_ == 2); my $encompassing = $self->_encompassing_view; while($encompassing) { if ($encompassing->subject eq $subject) { return 1; } else { $encompassing = $encompassing->_encompassing_view(); } } return; } sub all_subject_classes { my $self = shift; my @classes = (); # suppress error callbacks inside this method my $old_cb = UR::ModuleBase->message_callback('error'); UR::ModuleBase->message_callback('error', sub {}) if ($old_cb); for my $aspect ($self->aspects) { unless ($aspect->delegate_view) { eval { $aspect->generate_delegate_view; }; } if ($aspect->delegate_view) { push @classes, $aspect->delegate_view->all_subject_classes } } UR::ModuleBase->message_callback('error', $old_cb) if ($old_cb); push @classes, $self->subject_class_name; my %saw; my @uclasses = grep(!$saw{$_}++,@classes); return @uclasses; } sub all_subject_classes_ancestry { my $self = shift; my @classes = $self->all_subject_classes; my @aclasses; for my $class (@classes) { my $m; eval { $m = $class->__meta__ }; next if $@ or not $m; push @aclasses, reverse($class, $m->ancestry_class_names); } my %saw; my @uaclasses = grep(!$saw{$_}++,@aclasses); return @uaclasses; } # rendering implementation sub widget { my $self = shift; if (@_) { Carp::confess("Widget() is not settable! Its value is set from _create_widget() upon first use."); } my $widget = $self->_widget(); unless ($widget) { $widget = $self->_create_widget(); return unless $widget; $self->_widget($widget); $self->_bind_subject(); # works even if subject is undef } return $widget; } sub _create_widget { Carp::confess("The _create_widget method must be implemented for all concrete " . " view subclasses. No _create_widget for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } sub _bind_subject { # This is called whenever the subject changes, or when the widget is first created. # It handles the case in which the subject is undef. my $self = shift; my $subject = $self->subject(); return unless defined $subject; my $observer_data = $self->_observer_data; unless ($observer_data) { $self->_observer_data({}); $observer_data = $self->_observer_data; } Carp::confess unless $self->_observer_data == $observer_data; # See if we've already done this. return 1 if $observer_data->{$subject}; # Wipe subscriptions from the last bound subscription(s). for (keys %$observer_data) { my $s = delete $observer_data->{$_}; my ($class, $id, $method,$callback) = @$s; $class->cancel_change_subscription($id, $method,$callback); } return unless $subject; # Make a new subscription for this subject my $subscription = $subject->create_subscription( callback => sub { $self->_update_view_from_subject(@_); } ); $observer_data->{$subject} = $subscription; # Set the view to show initial data. $self->_update_view_from_subject; return 1; } sub _update_view_from_subject { # This is called whenever the view changes, or the subject changes. # It passes the change(s) along, so that the update can be targeted, if the developer chooses. Carp::croak("The _update_view_from_subject method must be implemented for all concreate " . " view subclasses. No _update_subject_from_view for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } sub _update_subject_from_view { Carp::croak("The _update_subject_from_view method must be implemented for all concreate " . " view subclasses. No _update_subject_from_view for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } # external controls sub show { my $self = shift; $self->_toolkit_package->show_view($self); } sub show_modal { my $self = shift; $self->_toolkit_package->show_view_modally($self); } sub hide { my $self = shift; $self->_toolkit_package->hide_view($self); } sub _toolkit_package { my $self = shift; my $toolkit = $self->toolkit; return "UR::Object::View::Toolkit::" . ucfirst(lc($toolkit)); } 1; =pod =head1 NAME UR::Object::View - a base class for "views" of UR::Objects =head1 SYNOPSIS $object = Acme::Product->get(1234); ## Acme::Product::View::InventoryHistory::Gtk2 $view = $object->create_view( perspective => 'inventory history', toolkit => 'gtk2', ); $widget = $view->widget(); # returns the Gtk2::Widget itself directly $view->show(); # puts the widget in a Gtk2::Window and shows everything ## $view = $object->create_view( perspective => 'inventory history', toolkit => 'xml', ); $widget = $view->widget(); # returns an arrayref with the xml string reference, and the output filehandle (stdout) $view->show(); # prints the current xml content to the handle $xml = $view->content(); # returns the XML directly ## $view = $object->create_view( perspective => 'inventory history', toolkit => 'html', ); $widget = $view->widget(); # returns an arrayref with the html string reference, and the output filehandle (stdout) $view->show(); # prints the html content to the handle $html = $view->content(); # returns the HTML text directly =head1 USAGE API =over 4 =item create The constructor requires that the subject_class_name, perspective, and toolkit be set. Most concrete subclasses have perspective and toolkit set as constant. Producing a view object does not "render" the view, just creates an interface for controlling the view, including encapsualting its creation. The subject can be set later and changed. The aspects viewed may be constant for a given perspective, or mutable, depending on how flexible the of the perspective logic is. =item show For stand-alone views, this puts the view widget in its a window. For views which are part of a larger view, this makes the view widget visible in the parent. =item hide Makes the view invisible. This means hiding the window, or hiding the view widget in the parent widget for subordinate views. =item show_modal This method shows the view in a window, and only returns after the window is closed. It should only be used for views which are a full interface capable of closing itself when done. =item widget Returns the "widget" which renders the view. This is built lazily on demand. The actual object type depends on the toolkit named above. This method might return HTML text, or a Gtk object. This can be used directly, and is used internally by show/show_modal. (Note: see UR::Object::View::Toolkit::Text for details on the "text" widget, used by HTML/XML views, etc. This is just the content and an I/O handle to which it should stream.) =item delete Delete the view (along with the widget(s) and infrastructure underlying it). =back =head1 CONSTRUCTION PROPERTIES (CONSTANT) The following three properties are constant for a given view class. They determine which class of view to construct, and must be provided to create(). =over 4 =item subject_class_name The class of subject this view will view. Constant for any given view, but this may be any abstract class up-to UR::Object itself. =item perspective Used to describe the layout logic which gives logical content to the view. =item toolkit The specific (typically graphical) toolkit used to construct the UI. Examples are Gtk, Gkt2, Tk, HTML, XML. =back =head1 CONFIGURABLE PROPERTIES These methods control which object is being viewed, and what properties of the object are viewed. They can be provided at construction time, or afterward. =over 4 =item subject The particular "model" object, in MVC parlance, which is viewed by this view. This value may change =item aspects / add_aspect / remove_aspect Specifications for properties/methods of the subject which are rendered in the view. Some views have mutable aspects, while others merely report which aspects are revealed by the perspective in question. An "aspect" is some characteristic of the "subject" which is rendered in the view. Any property of the subject is usable, as is any method. =back =head1 IMPLEMENTATION INTERFACE When writing new view logic, the class name is expected to follow a formula: Acme::Rocket::View::FlightPath::Gtk2 \ / \ / \ subject class name perspective toolkit The toolkit is expected to be a single word. The perspective is everything before the toolkit, and after the last 'View' word. The subject_class_name is everything to the left of the final '::View::'. There are three methods which require an implementation, unless the developer inherits from a subclass of UR::Object::View which provides these methods: =over 4 =item _create_widget This creates the widget the first time ->widget() is called on a view. This should be implemented in a given perspective/toolkit module to actually create the GUI using the appropriate toolkit. It will be called before the specific subject is known, so all widget creation which is subject-specific should be done in _bind_subject(). As such it typically only configures skeletal aspects of the view. =item _bind_subject This method is called when the subject is set, or when it is changed, or unset. It updates the widget to reflect changes to the widget due to a change in subject. This method has a default implementation which does a general subscription to changes on the subject. It probably does not need to be overridden in custom views. Implementations which _do_ override this should take an undef subject, and be sure to un-bind a previously existing subject if there is one set. =item _update_view_from_subject If and when the property values of the subject change, this method will be called on all views which render the changed aspect of the subject. =item _update_subject_from_view When the widget changes, it should call this method to save the UI changes to the subject. This is not applicable to read-only views. =back =head1 OTHER METHODS =over 4 =item _toolkit_package This method is useful to provide generic toolkit-based services to a view, using a toolkit agnostic API. It can be used in abstract classes which, for instance, want to share logic for a given perspective across toolkits. The toolkit class related to a view is responsible for handling show/hide logic, etc. in the base UR::Object::View class. Returns the name of a class which is derived from UR::Object::View::Toolkit which implements certain utility methods for views of a given toolkit. =back =head1 EXAMPLES $o = Acme::Product->get(1234); $v = Acme::Product::View::InventoryHistory::HTML->create(); $v->add_aspect('outstanding_orders'); $v->show; =cut Aspect.pm100664023532023421 2265312544604516 16656 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/Viewpackage UR::Object::View::Aspect; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; class UR::Object::View::Aspect { id_by => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => "the id of the view object this is an aspect-of" }, number => { is => 'Integer', doc => "aspects of a view are numbered" }, ], has => [ name => { is => 'Text', is_mutable => 0, doc => 'the name of the property/method on the subject which returns the value to be viewed' }, ], has_optional => [ label => { is => 'Text', doc => 'display name for this aspect' }, position => { is => 'Scalar', doc => 'position of this aspect within the parent view (meaning is view and toolkit dependent)' }, delegate_view => { is => 'UR::Object::View', id_by => 'delegate_view_id', doc => "This aspect gets rendered via another view" }, ], }; sub create { my $class = shift; my($bx,%extra) = $class->define_boolexpr(@_); # TODO: it would be nice to have this in the class definition: # increment_for => 'parent_view' unless ($bx->value_for('number')) { if (my $parent_view_id = $bx->value_for('parent_view_id')) { my $parent_view = UR::Object::View->get($parent_view_id); my @previous_aspects = $parent_view->aspects; $bx = $bx->add_filter(number => scalar(@previous_aspects)+1); } } unless ($bx->value_for('label')) { if (my $label = $bx->value_for('name')) { $label =~ s/_/ /g; $bx = $bx->add_filter(label => $label); } } if (keys %extra) { # This is a sub-view my $delegate_subject_class_name; if (exists $extra{'subject_class_name'}) { $delegate_subject_class_name = $extra{'subject_class_name'}; } else { # FIXME This duplicates functionality below in generate_delegate_view, but generate_delegate_view() # doesn't take any args to tweak the properties of that delegated view :( # Try to figure it out based on the name of the aspect... my $parent_view; if (my $view_id = $bx->value_for('parent_view_id')) { $parent_view = UR::Object::View->get($view_id); } elsif ($bx->specifies_value_for('parent_view')) { $parent_view = $bx->value_for('parent_view'); } unless ($parent_view) { Carp::croak("Can't determine parent view from keys/values: ",join(', ', map { sprintf("%s => '%s'", $_, $extra{$_}) } keys %extra)); } my $class_meta = $parent_view->subject_class_name->__meta__; unless ($class_meta) { Carp::croak("No class metadata for class " . $parent_view->subject_class_meta . ". Can't create delegate view on aspect named " . $bx->value_for('name') ); } my $property_meta = $class_meta->property_meta_for_name($bx->value_for('name')); unless ($property_meta) { Carp::croak("No property metadata for class " . $class_meta->class_name . " property " . $bx->value_for('name') . ". Can't create delegate view on aspect named " . $bx->value_for('name')); } unless ($property_meta->data_type) { Carp::croak("Property metadata for class " . $class_meta->class_name . " property " . $property_meta->property_name . " has no data_type. Can't create delegate view on aspect named " . $bx->value_for('name')); } $delegate_subject_class_name = $property_meta->data_type; } unless ($delegate_subject_class_name) { Carp::croak("Can't determine subject_class_name for delegate view on aspect named " . $bx->value_for('name')); } my $delegate_view = $delegate_subject_class_name->create_view( perspective => $bx->value_for('perspective'), toolkit => $bx->value_for('toolkit'), %extra ); unless ($delegate_view) { Carp::croak("Can't create delegate view for aspect named " . $bx->value_for('name') . ": ".$delegate_subject_class_name->error_message); } #$bx->add_filter(delegate_view_id => $delegate_view->id); $bx = $bx->add_filter(delegate_view => $delegate_view); } my $self = $class->SUPER::create($bx); return unless $self; my $name = $self->name; unless ($name) { $self->error_message("No name specified for aspect!"); $self->delete; return; } return $self; } sub _look_for_recursion { my $self = shift; my $parent_view = $self->parent_view; my $subject = $parent_view->subject; $parent_view = $parent_view->parent_view; while($parent_view) { return 1 if ($parent_view->subject eq $subject); $parent_view = $parent_view->parent_view; } return 0; } sub generate_delegate_view { no warnings; my $self = shift; my $parent_view = $self->parent_view; my $name = $self->name; my $subject_class_name = $parent_view->subject_class_name; my $retval; my $property_meta = $subject_class_name->__meta__->property($name); my $aspect_type; if ($property_meta) { $aspect_type = $property_meta->_data_type_as_class_name; unless ($aspect_type) { Carp::confess("Undefined aspect type. Set 'is' for $name in class " . $property_meta->class_name); } unless ($aspect_type) { if (my $delegated_to_meta = $property_meta->final_property_meta) { $aspect_type = $delegated_to_meta->data_type; } } unless ($aspect_type) { Carp::confess("Property meta for class ".$property_meta->class_name." property ".$property_meta->property_name." has no data_type"); } unless ($aspect_type->can("__meta__")) { Carp::croak("$aspect_type has no meta data? cannot generate a view for $subject_class_name $name!"); } } else { unless ($subject_class_name->can($name)) { $self->error_message("No property/method $name found on $subject_class_name! Invalid aspect!"); $self->delete; Carp::croak($self->error_message); } $aspect_type = 'UR::Value::Text' } my $aspect_meta = $aspect_type->__meta__; my $delegate_view; eval { $delegate_view = $aspect_type->create_view( subject_class_name => $aspect_type, perspective => $parent_view->perspective, toolkit => $parent_view->toolkit, parent_view => $parent_view, aspects => [], ); }; unless ($delegate_view) { # try again using the "default" perspective my $err1 = $@; eval { $delegate_view = $aspect_type->create_view( subject_class_name => $aspect_type, perspective => 'default', toolkit => $parent_view->toolkit, parent_view => $parent_view, aspects => [], ); }; my $err2 = $@; unless ($delegate_view) { $self->error_message( "Error creating delegate view for $name ($aspect_type)! $err1\n" . "Also failed to fall back to the default perspective for $name ($aspect_type)! $err2" ); return; } } my @default_aspects_params = $delegate_view->_resolve_default_aspects(); # add aspects which do not "go backward" # no one wants to see an order, with a list of line items, which re-reprsent thier order on each for my $aspect_params (@default_aspects_params) { my $aspect_param_name = (ref($aspect_params) ? $aspect_params->{name} : $aspect_params); my $aspect_property_meta = $aspect_meta->property($aspect_param_name); no strict; no warnings; next if (!$aspect_property_meta or !$property_meta); if ($aspect_property_meta->reverse_as() eq $name) { } elsif ($property_meta->reverse_as eq $aspect_param_name) { } else { $delegate_view->add_aspect(ref($aspect_params) ? %$aspect_params : $aspect_params); } } $self->delegate_view($delegate_view); $retval = $delegate_view; return $retval; } 1; =pod =head1 NAME UR::Object::View::Aspect - a specification for one aspect of a view =head1 SYNOPSIS my $v = $o->create_view( perspective => 'default', toolkit => 'xml', aspects => [ 'id', 'name', 'title', { name => 'department', perspective => 'logo' }, { name => 'boss', label => 'Supervisor', aspects => [ 'name', 'title', { name => 'subordinates', perspective => 'graph by title' } ] } ] ); =cut Gtk.pm100664023532023421 402612544604516 17522 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Gtk; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View', has_constant => [ perspective => { value => 'default' }, toolkit => { value => 'gtk' }, ] ); sub _create_widget { my $self = shift; my $label = Gtk::Label->new(""); return $label; } sub _update_view_from_subject { my $self = shift; my @changes = @_; # this is not currently resolved and passed-in my $subject = $self->subject(); my @aspects = $self->aspects; my $widget = $self->widget(); my $text = $self->subject_class_name; $text .= " with id " . $subject->id if $subject; # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->position, $_ ] } @aspects; for my $aspect (@sorted_aspects) { my $label = $aspect->label; $text .= "\n" . $label . ": "; if ($subject) { my @value = $subject->$label; $text .= join(", ", @value); } else { $text .= "-"; } } } $widget->set_text($text); return 1; } sub _update_subject_from_view { Carp::confess("This widget shouldn't be able to write to the object, it's a label? How did I get called?"); } sub _add_aspect { shift->_update_view_from_subject; } sub _remove_aspect { shift->_update_view_from_subject; } 1; =pod =head1 NAME UR::Object::View::Default::Gtk - Gtk adaptor for object views =head1 DESCRIPTION This class provides code that implements a basic Gtk renderer for UR objects. =head1 SEE ALSO UR::Object::View, UR::Object =cut Gtk2.pm100664023532023421 367712544604516 17617 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Gtk2; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::View::Default::Gtk2 { is => 'UR::Object::View', has_constant => [ perspective => { value => 'default'}, toolkit => { value => 'gtk2'}, ], }; sub _create_widget { my $self = shift; my $label = Gtk2::Label->new(""); return $label; } sub _update_view_from_subject { my $self = shift; my $subject = $self->subject(); my @aspects = $self->aspects; my $widget = $self->widget(); my $text = $self->subject_class_name; $text .= " with id " . $subject->id if $subject; # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->position, $_ ] } @aspects; for my $aspect (@sorted_aspects) { my $label = $aspect->label; $text .= "\n" . $label . ": "; if ($subject) { my @value = $subject->$label; $text .= join(", ", @value); } else { $text .= "-"; } } } $widget->set_text($text); return 1; } sub _update_subject_from_view { Carp::confess("This widget shouldn't be able to write to the object, it's a label? How did I get called?"); } sub _add_aspect { shift->_update_view_from_subject; } sub _remove_aspect { shift->_update_view_from_subject; } 1; =pod =head1 NAME UR::Object::View::Default::Gtk2 - Gtk2 adaptor for object views =head1 DESCRIPTION This class provides code that implements a basic Gtk2 renderer for UR objects. =head1 SEE ALSO UR::Object::View, UR::Object =cut Html.pm100664023532023421 272612544604516 17706 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Html; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; class UR::Object::View::Default::Html { is => 'UR::Object::View::Default::Xsl', has => { output_format => { value => 'html' }, transform => { value => 1 }, toolkit => { value => 'html' }, } }; 1; =pod =head1 NAME UR::Object::View::Default::Html - represent object state in HTML format =head1 SYNOPSIS ##### package Acme::Product::View::OrderStatus::Html; class Acme::Product::View::OrderStatus::Html { is => 'UR::Object::View::Default::Html', }; sub _generate_content { my $self = shift; my $subject = $self->subject; my $html = ... .... return $html; } ##### $o = Acme::Product->get(1234); $v = $o->create_view( perspective => 'order status', toolkit => 'html', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $html1 = $v->content; $o->qty_on_hand(200); $html2 = $v->content; =head1 DESCRIPTION This class implements basic HTML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Json.pm100664023532023421 645312544604516 17714 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Json; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use JSON; class UR::Object::View::Default::Json { is => 'UR::Object::View::Default::Text', has_constant => [ toolkit => { value => 'json' }, ], has_optional => [ encode_options => { is => 'ARRAY', default_value => ['ascii', 'pretty', 'allow_nonref', 'canonical'], doc => 'Options to enable on the JSON object; see the documentation for the JSON Perl module' }, ], }; my $json; sub _json { my ($self) = @_; return $json if defined $json; $json = JSON->new; foreach my $opt ( @{ $self->encode_options } ) { eval { $json = $json->$opt; }; if ($@) { Carp::croak("Can't initialize JSON object for encoding. Calling method $opt from encode_options died: $@"); } if (!$json) { Carp::croak("Can't initialize JSON object for encoding. Calling method $opt from encode_options returned false"); } } return $json; } sub _generate_content { my $self = shift; return $self->_json->encode($self->_jsobj); } sub _jsobj { my $self = shift; my $subject = $self->subject(); return '' unless $subject; my %jsobj = (); for my $aspect ($self->aspects) { my $val = $self->_generate_content_for_aspect($aspect); $jsobj{$aspect->name} = $val if defined $val; } return \%jsobj; } sub _generate_content_for_aspect { my $self = shift; my $aspect = shift; my $subject = $self->subject; my $aspect_name = $aspect->name; my $aspect_meta = $self->subject_class_name->__meta__->property($aspect_name); #warn $aspect_name if ref($subject) =~ /Set/; my @value; eval { @value = $subject->$aspect_name; }; if ($@) { warn $@; return; } # Always look for a delegate view. # This means we replace the value(s) with their # subordinate widget content. unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } my $ref = []; if (my $delegate_view = $aspect->delegate_view) { foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); if ($delegate_view->can('_jsobj')) { push @$ref, $delegate_view->_jsobj; } else { my $delegate_text = $delegate_view->content(); push @$ref, $delegate_text; } } } else { for my $value (@value) { if (ref($value)) { push @$ref, 'ref'; #TODO(ec) make this render references } else { push @$ref, $value; } } } if ($aspect_meta && $aspect_meta->is_many) { return $ref; } else { return shift @$ref; } } # Do not return any aspects by default if we're embedded in another view # The creator of the view will have to specify them manually sub _resolve_default_aspects { my $self = shift; unless ($self->parent_view) { return $self->SUPER::_resolve_default_aspects; } return ('id'); } 1; Text.pm100664023532023421 1737212544604516 17751 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::View::Default::Text { is => 'UR::Object::View', has_constant => [ perspective => { value => 'default' }, toolkit => { value => 'text' }, ], has => [ indent_text => { is => 'Text', default_value => ' ', doc => 'indent child views with this text' }, ], }; # general view API sub _create_widget { # The "widget" for a text view is a pair of items: # - a scalar reference to hold the content # - an I/O handle to which it will display (the "window" it lives in) # Note that the former could be something tied to an object, # a file, or other external storage, though it is # simple by default. The later might also be tied. # The later is STDOUT unless overridden/changed. my $self = shift; my $scalar_ref = ''; my $fh = 'STDOUT'; return [\$scalar_ref,$fh]; } sub show { # Showing a text view typically prints to STDOUT my $self = shift; my $widget = $self->widget(); my ($content_ref,$output_stream) = @$widget; $output_stream->print($$content_ref,"\n"); } sub _update_subject_from_view { Carp::confess('currently text views are read-only!'); } sub _update_view_from_subject { my $self = shift; my $content = $self->_generate_content(@_); my $widget = $self->widget(); my ($content_ref,$fh) = @$widget; $$content_ref = $content; return 1; } # text view API sub content { # retuns the current value of the scalar ref containing the text content. my $self = shift; my $widget = $self->widget(); if (@_) { die "the widget reference for a view isn't changeable. change its content.."; } my ($content_ref,$output_stream) = @$widget; return $$content_ref; } sub output_stream { # retuns the current value of the handle to which we render. my $self = shift; my $widget = $self->widget(); if (@_) { return $widget->[1] = shift; } my ($content_ref,$output_stream) = @$widget; return $output_stream; } sub _generate_content { my $self = shift; # the header line is the class followed by the id my $text = $self->subject_class_name; $text =~ s/::/ /g; my $subject = $self->subject(); if ($subject) { my $subject_id_txt = $subject->id; $subject_id_txt = "'$subject_id_txt'" if $subject_id_txt =~ /\s/; $text .= " $subject_id_txt"; } # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; # the content for any given aspect is handled separately my @aspects = $self->aspects; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->number, $_ ] } @aspects; for my $aspect (@sorted_aspects) { next if $aspect->name eq 'id'; my $aspect_text = $self->_generate_content_for_aspect($aspect); $text .= $aspect_text; } } return $text; } sub _generate_content_for_aspect { # This does two odd things: # 1. It gets the value(s) for an aspect, then expects to just print them # unless there is a delegate view. In which case, it replaces them # with the delegate's content. # 2. In cases where more than one value is returned, it recycles the same # view and keeps the content. # # These shortcuts make it hard to abstract out logic from toolkit-specifics my $self = shift; my $aspect = shift; my $subject = $self->subject; my $indent_text = $self->indent_text; my $aspect_text = $indent_text . $aspect->label . ": "; if (!$subject) { $aspect_text .= "-\n"; return $aspect_text; } my $aspect_name = $aspect->name; my @value; eval { @value = $subject->$aspect_name; }; if (@value == 0) { $aspect_text .= "-\n"; return $aspect_text; } if (@value == 1 and ref($value[0]) eq 'ARRAY') { @value = @{$value[0]}; } unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } # Delegate to a subordinate view if needed. # This means we replace the value(s) with their # subordinate widget content. if (my $delegate_view = $aspect->delegate_view) { # TODO: it is bad to recycle a view here?? # Switch to a set view, which is the standard lister. foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); $value = $delegate_view->content(); } } if (@value == 1 and defined($value[0]) and index($value[0],"\n") == -1) { # one item, one row in the value or sub-view of the item: $aspect_text .= $value[0] . "\n"; } else { my $aspect_indent; if (@value == 1) { # one level of indent for this sub-view's sub-aspects # zero added indent for the identity line b/c it's next-to the field label # aspect1: class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_indent = $indent_text; } else { # two levels of indent for this sub-view's sub-aspects # just one level for each identity # aspect1: ... # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_text .= "\n"; $aspect_indent = $indent_text . $indent_text; } for my $value (@value) { my $value_indented = ''; if (defined $value) { my @rows = split(/\n/,$value); $value_indented = join("\n", map { $aspect_indent . $_ } @rows); chomp $value_indented; } $aspect_text .= $value_indented . "\n"; } } return $aspect_text; } 1; =pod =head1 NAME UR::Object::View::Default::Text - object views in text format =head1 SYNOPSIS $o = Acme::Product->get(1234); # generates a UR::Object::View::Default::Text object: $v = $o->create_view( toolkit => 'text', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $txt1 = $v->content; $o->qty_on_hand(200); $txt2 = $v->content; =head1 DESCRIPTION This class implements basic text views of objects. It is used for command-line tools, and is the base class for other specific text formats like XML, HTML, JSON, etc. =head1 WRITING A SUBCLASS # In Acme/Product/View/OutstandingOrders/Text.pm package Acme::Product::View::OutstandingOrders::Text; use UR; class Acme::Product::View::OutstandingOrders::Text { is => 'UR::Object::View::Default::Text' }; sub _initial_aspects { return ( 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ); } $v = $o->create_view(perspective => 'outstanding orders', toolkit => 'text'); print $v->content; =head1 SEE ALSO UR::Object::View, UR::Object::View::Toolkit::Text, UR::Object =cut Xml.pm100664023532023421 2354512544604516 17564 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; use XML::Dumper; use XML::LibXML; class UR::Object::View::Default::Xml { is => 'UR::Object::View::Default::Text', has_constant => [ toolkit => { value => 'xml' }, ], has => [ _xml_doc => { is => 'XML::LibXML::Document', doc => 'The LibXML document used to create the content for this view', is_transient => 1 } ], }; sub xsl_template_files { my $self = shift; #usually this is a view without a subject attached my $output_format = shift; my $root_path = shift; my $perspective = shift || lc($self->perspective); my @xsl_names = map { $_ =~ s/::/_/g; my $pf = "/$output_format/$perspective/" . lc($_) . '.xsl'; my $df = "/$output_format/default/" . lc($_) . '.xsl'; -e $root_path . $pf ? $pf : (-e $root_path . $df ? $df : undef) } $self->all_subject_classes_ancestry; my @found_xsl_names = grep { defined } @xsl_names; return @found_xsl_names; } sub _generate_xml_doc { my $self = shift; my $subject = $self->subject(); return unless $subject; my $xml_doc = XML::LibXML->createDocument(); $self->_xml_doc($xml_doc); # the header line is the class followed by the id my $object = $xml_doc->createElement('object'); $xml_doc->setDocumentElement($object); $object->addChild( $xml_doc->createAttribute('type', $self->subject_class_name) ); $object->addChild( $xml_doc->createAttribute('id', $subject->id ) ); my $display_name = $object->addChild( $xml_doc->createElement('display_name') ); $display_name->addChild( $xml_doc->createTextNode($subject->__display_name__) ); my $label_name = $object->addChild( $xml_doc->createElement('label_name' )); $label_name->addChild( $xml_doc->createTextNode($subject->__label_name__) ); my $types = $object->addChild( $xml_doc->createElement('types') ); foreach my $c ($self->subject_class_name,$subject->__meta__->ancestry_class_names) { my $isa = $types->addChild( $xml_doc->createElement('isa') ); $isa->addChild( $xml_doc->createAttribute('type', $c) ); } unless ($self->_subject_is_used_in_an_encompassing_view()) { # the content for any given aspect is handled separately my @aspects = $self->aspects; if (@aspects) { my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->number, $_ ] } @aspects; for my $aspect (@sorted_aspects) { next if $aspect->name eq 'id'; my $aspect_node = $self->_generate_content_for_aspect($aspect); $object->addChild( $aspect_node ) if $aspect_node; #If aspect has no values, it won't be included } } } #From the XML::LibXML documentation: #If $format is 1, libxml2 will add ignorable white spaces, so the nodes content is easier to read. Existing text nodes will not be altered #If $format is 2 (or higher), libxml2 will act as $format == 1 but it add a leading and a trailing line break to each text node. return $xml_doc; } sub _generate_content { my $self = shift; my $xml_doc = $self->_generate_xml_doc; return '' unless $xml_doc; my $doc_string = $xml_doc->toString(1); # remove invalid XML entities $doc_string =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go; return $doc_string; } sub _add_perl_data_to_node { my $self = shift; my $perlref = shift; my $node = shift; my $xml_doc = $self->_xml_doc; $node ||= $xml_doc->documentElement; my $d = XML::Dumper->new; my $perldata = $d->pl2xml($perlref); my $parser = XML::LibXML->new; my $ref_xml_doc = $parser->parse_string($perldata); my $ref_root = $ref_xml_doc->documentElement; $xml_doc->adoptNode( $ref_root ); $node->addChild( $ref_root ); return 1; } sub _generate_content_for_aspect { # This does two odd things: # 1. It gets the value(s) for an aspect, then expects to just print them # unless there is a delegate view. In which case, it replaces them # with the delegate's content. # 2. In cases where more than one value is returned, it recycles the same # view and keeps the content. # # These shortcuts make it hard to abstract out logic from toolkit-specifics my $self = shift; my $aspect = shift; my $subject = $self->subject; my $xml_doc = $self->_xml_doc; my $aspect_name = $aspect->name; my $aspect_node = $xml_doc->createElement('aspect'); $aspect_node->addChild( $xml_doc->createAttribute('name', $aspect_name) ); my @value; eval { @value = $subject->$aspect_name; }; if ($@) { my ($file,$line) = ($@ =~ /at (.*?) line (\d+)$/m); my $exception = $aspect_node->addChild( $xml_doc->createElement('exception') ); $exception->addChild( $xml_doc->createAttribute('file', $file) ); $exception->addChild( $xml_doc->createAttribute('line', $line) ); $exception->addChild( $xml_doc->createCDATASection($@) ); return $aspect_node; } if (not Scalar::Util::blessed($value[0])) { # shortcut to optimize for simple scalar values without delegate views for my $value ( @value ) { my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); $value = '' if not defined $value; $value_node->addChild( $xml_doc->createTextNode($value) ); } return $aspect_node; } unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } # Delegate to a subordinate view if needed. # This means we replace the value(s) with their # subordinate widget content. my $delegate_view = $aspect->delegate_view; unless ($delegate_view) { Carp::confess("No delegate view???"); } foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); # merge the delegate view's XML into this one if ($delegate_view->can('_xml_doc') and $delegate_view->_xml_doc) { # the delegate has XML my $delegate_xml_doc = $delegate_view->_xml_doc; my $delegate_root = $delegate_xml_doc->documentElement; #cloneNode($deep = 1) $aspect_node->addChild( $delegate_root->cloneNode(1) ); } elsif (ref($value) and not $value->isa("UR::Value")) { # Note: Let UR::Values display content below # Otherwise, the delegate view has no XML object, and the value is a reference $self->_add_perl_data_to_node($value, $aspect_node); } elsif (ref($value) and $value->isa("UR::Value")) { # For a UR::Value return both a formatted value and a raw value. my $display_value_node = $aspect_node->addChild( $xml_doc->createElement('display_value') ); my $content = $delegate_view->content; $content = '' if not defined $content; $display_value_node->addChild( $xml_doc->createTextNode($content) ); my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); $content = $value->id; $value_node->addChild( $xml_doc->createTextNode($content) ); } else { # no delegate view has no XML object, and the value is a non-reference # (this is the old logic for non-delegate views when we didn't have delegate views for primitives) my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); unless(defined $value) { $value = ''; } my $content = $delegate_view->content; $content = '' if not defined $content; $value_node->addChild( $xml_doc->createTextNode($content) ); ## old logic for delegate views with no xml doc (unused now) ## the delegate view may not be XML at all--wrap it in our aspect tag so that it parses ## (assuming that whatever delegate was selected properly escapes anything that needs escaping) # my $delegate_text = $delegate_view->content() ? $delegate_view->content() : ''; # my $aspect_text = "\n$delegate_text\n"; # my $parser = XML::LibXML->new; # my $delegate_xml_doc = $parser->parse_string($aspect_text); # $aspect_node = $delegate_xml_doc->documentElement; # $xml_doc->adoptNode( $aspect_node ); } } return $aspect_node; } # Do not return any aspects by default if we're embedded in another view # The creator of the view will have to specify them manually sub _resolve_default_aspects { my $self = shift; unless ($self->parent_view) { return $self->SUPER::_resolve_default_aspects; } return; } 1; =pod =head1 NAME UR::Object::View::Default::Xml - represent object state in XML format =head1 SYNOPSIS $o = Acme::Product->get(1234); $v = $o->create_view( toolkit => 'xml', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $xml1 = $v->content; $o->qty_on_hand(200); $xml2 = $v->content; =head1 DESCRIPTION This class implements basic XML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Xsl.pm100664023532023421 1716012544604516 17566 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Xsl; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; use XML::LibXML; use XML::LibXSLT; class UR::Object::View::Default::Xsl { is => 'UR::Object::View::Default::Text', has => [ output_format => { value => 'html' }, transform => { is => 'Boolean', value => 0 }, xsl_variables => { is => 'Hash', is_optional => 1 }, rest_variable => { value => '/rest', is_deprecated => 1 }, desired_perspective => { }, xsl_path => { doc => 'web relative path starting with / where the xsl ' . 'is located when serving from a web service' }, xsl_root => { doc => 'absolute path where xsl files will be found, expected ' . 'format is $xsl_path/$output_format/$perspective/' . '$normalized_class_name.xsl' }, ] }; use Exporter 'import'; our @EXPORT_OK = qw(type_to_url url_to_type); sub _generate_content { my ($self, %params) = @_; if (!$self->desired_perspective) { $self->desired_perspective($self->perspective); } # my $subject = $self->subject; # return unless $subject; unless ($self->xsl_root && -e $self->xsl_root) { die 'xsl_root does not exist:' . $self->xsl_root; } my $xml_view = $self->_get_xml_view(%params); # my $xml_content = $xml_view->_generate_content(); my $doc = $self->_generate_xsl_doc($xml_view); if ($self->transform) { return $self->transform_xml($xml_view,$doc); #$xsl_template); } else { return $doc->toString(1); # $xsl_template; } } sub _get_xml_view { my $self = shift; my %params = @_; # get the xml for the equivalent perspective my $xml_view; eval { $xml_view = UR::Object::View->create( subject_class_name => $self->subject_class_name, perspective => $self->desired_perspective, toolkit => 'xml', %params ); }; if ($@) { # try again, for debugging, don't hate me for this $DB::single you're about to crash.. $DB::single = 1; $xml_view = UR::Object::View->create( subject_class_name => $self->subject_class_name, perspective => $self->perspective, toolkit => 'xml', %params ); } return $xml_view; } sub _generate_xsl_doc { my $self = shift; my $xml_view = shift; # subclasses typically have this as a constant value # it turns out we don't need it, since the file will be HTML.pm.xsl for xml->html conversion # my $toolkit = $self->toolkit; my $output_format = $self->output_format; my $xsl_path = $self->xsl_root; unless ($self->transform) { # when not transforming we'll return a relative path # suitable for urls $xsl_path = $self->xsl_path; } my $perspective = $self->desired_perspective; my @include_files = $self->_resolve_xsl_template_files( $xml_view, $output_format, $xsl_path, $perspective ); my $rootxsl = "/$output_format/$perspective/root.xsl"; if (!-e $xsl_path . $rootxsl) { $rootxsl = "/$output_format/default/root.xsl"; } my $commonxsl = "/$output_format/common.xsl"; if (-e $xsl_path . $commonxsl) { push(@include_files, $commonxsl); } no warnings; my $xslns = 'http://www.w3.org/1999/XSL/Transform'; my $doc = XML::LibXML::Document->new("1.0", "ISO-8859-1"); my $ss = $doc->createElementNS($xslns, 'stylesheet'); $ss->setAttribute('version', '1.0'); $doc->setDocumentElement($ss); $ss->setNamespace($xslns, 'xsl', 1); my $time = time . "000"; ## this is the wrong place for this information # since it is already part of the XML document # it shouldn't be hard coded into the transform my $display_name = $self->subject->__display_name__; my $label_name = $self->subject->__label_name__; my $set_var = sub { my $e = $doc->createElementNS($xslns, 'param'); $e->setAttribute('name', $_[0]); $e->appendChild( $doc->createTextNode( $_[1] ) ); $ss->appendChild($e) }; $set_var->('currentPerspective',$perspective); $set_var->('currentToolkit',$output_format); $set_var->('displayName',$display_name); $set_var->('labelName',$label_name); $set_var->('currentTime',$time); $set_var->('username',$ENV{'REMOTE_USER'}); if (my $id = $self->subject->id) { $set_var->('objectId', $id); } if (my $class_name = $self->subject->class) { $set_var->('objectClassName', $class_name); } if (my $vars = $self->xsl_variables) { while (my ($key,$val) = each %$vars) { $set_var->($key, $val); } } else { $set_var->('rest',$self->rest_variable); } my $rootn = $doc->createElementNS($xslns, 'include'); $rootn->setAttribute('href',"$xsl_path$rootxsl"); $ss->appendChild($rootn); for (@include_files) { my $e = $doc->createElementNS($xslns, 'include'); $e->setAttribute('href',"$xsl_path$_"); $ss->appendChild($e) } return $doc; } sub _resolve_xsl_template_files { my ($self, $xml_view, $output_format, $xsl_path, $perspective) = @_; return $xml_view->xsl_template_files( $output_format, $xsl_path, $perspective, ); } sub transform_xml { my ($self,$xml_view,$style_doc) = @_; $xml_view->subject($self->subject); my $xml_content = $xml_view->_generate_content(); # remove invalid XML entities $xml_content =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go; my $parser = XML::LibXML->new; my $xslt = XML::LibXSLT->new; my $source; if($xml_view->can('_xml_doc') and $xml_view->_xml_doc) { $source = $xml_view->_xml_doc; } else { $source = $parser->parse_string($xml_content); } # convert the xml my $stylesheet = $xslt->parse_stylesheet($style_doc); my $results = $stylesheet->transform($source); my $content = $stylesheet->output_string($results); return $content; } sub type_to_url { join( '/', map { s/(?register_function( 'urn:rest', 'typetourl', \&type_to_url ); XML::LibXSLT->register_function( 'urn:rest', 'urltotype', \&url_to_type ); 1; =pod =head1 NAME UR::Object::View::Default::Xsl - base class for views which use XSL on an XML view to generate content =head1 SYNOPSIS ##### class Acme::Product::View::OrderStatus::Html { is => 'UR::Object::View::Default::Xsl', } ##### Acme/Product/View/OrderStatus/Html.pm.xsl ##### $o = Acme::Product->get(1234); $v = $o->create_view( perspective => 'order status', toolkit => 'html', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $xml1 = $v->content; $o->qty_on_hand(200); $xml2 = $v->content; =head1 DESCRIPTION This class implements basic HTML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Text.pm100664023532023421 731712544604516 17605 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Listerpackage UR::Object::View::Lister::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', ); sub _update_view_from_subject { my $self = shift; my @changes = @_; # this is not currently resolved and passed-in my $subject = $self->subject(); my $subject_class_meta = $subject->__meta__; my @aspects = $self->aspects; my %data_for_this_object; my(%aspects_requiring_joins_by_name,%aspects_requiring_joins_by_via); my %column_for_label; for (my $i = 0; $i < @aspects; $i++) { my $aspect = $aspects[$i]; my $label = $aspect->label; my $aspect_name = $aspect->name; $column_for_label{$label} = $i; my $property_meta = $subject_class_meta->property_meta_for_name($aspect_name); if (my $via = $property_meta->via and $property_meta->is_many) { $aspects_requiring_joins_by_name{$aspect_name} = $via; $aspects_requiring_joins_by_via{$via} ||= []; push @{$aspects_requiring_joins_by_via{$via}}, $aspect_name; } if ($subject) { my @value = $subject->$aspect_name; if (@value == 1 and ref($value[0]) eq 'ARRAY') { @value = @{$value[0]}; } # Delegate to a subordinate view if need be if ($aspect->delegate_view_id) { my $delegate_view = $aspect->delegate_view; foreach my $value ( @value ) { $delegate_view->subject($value); $delegate_view->_update_view_from_subject(); $value = $delegate_view->content(); } } if (@value == 1) { $data_for_this_object{$label} = $value[0]; } else { $data_for_this_object{$label} = \@value; } } } if (keys(%aspects_requiring_joins_by_via) > 1) { $self->error_message("Viewing delegated properties via more than one property is not supported"); return; } # fill in the first row of data my @retval = (); foreach my $aspect ( @aspects ) { my $label = $aspect->label; my $col = $column_for_label{$label}; if (ref($data_for_this_object{$label})) { # it's a multi-value $retval[0]->[$col] = shift @{$data_for_this_object{$label}}; } else { $retval[0]->[$col] = $data_for_this_object{$label}; } } foreach my $via ( keys %aspects_requiring_joins_by_via ) { while(1) { my @this_row; foreach my $prop ( @{$aspects_requiring_joins_by_via{$via}} ) { my $data; if (ref($data_for_this_object{$prop}) eq 'ARRAY') { $data = shift @{$data_for_this_object{$prop}}; next unless $data; } else { $data = $data_for_this_object{$prop}; $data_for_this_object{$prop} = []; } $this_row[$column_for_label{$prop}] = $data; } last unless @this_row; push @retval, \@this_row; } } foreach my $row ( @retval ) { no warnings 'uninitialized'; $row = join("\t",@$row); } my $text = join("\n", @retval); # The text widget won't print anything until show(), # so store the data in the buffer for now my $widget = $self->widget; ${$widget->[0]} = $text; # Update the contents return 1; } sub _update_subject_from_view { 1; } sub _add_aspect { 1; } sub _remove_aspect { 1; } 1; Html.pm100664023532023421 332412544604516 17544 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Staticpackage UR::Object::View::Static::Html; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Object::View::Static::Html { is => 'UR::Object::View', has => { output_format => { value => 'html' }, html_root => { doc => 'path to plain-old html files' } }, has_constant => [ perspective => { value => 'static' }, toolkit => { value => 'html' }, ], }; sub content { my ($self) = @_; my $filename = class_to_filename($self->subject->class); my $perspective = $self->perspective() || die "Error: I have no perspective"; my $pathname = join('/', $self->html_root(), $perspective, $filename); open(my $fh, $pathname); if (!$fh) { die "Could not open the static html file: $pathname"; } my $c = do { undef $/; <$fh>; }; close($pathname); return $c; } sub class_to_filename { my ($class) = @_; $class = lc($class); $class =~ s/::/_/g; $class .= '.html'; return $class; } 1; =pod =head1 NAME UR::Object::View::Static::Html - represent object state in HTML format =head1 SYNOPSIS package Genome::Sample::Set::View::Detail::Html; class Genome::Sample::Set::View::Detail::Html { is => 'UR::Object::View::Static::Html', has_constant => [ toolkit => { value => 'html' }, perspective => { value => 'detail' } ] }; =head1 DESCRIPTION The current default HTML class creates HTML by getting XML and applying XSL. This class, on the other hand, displays some static html =head1 SEE ALSO UR::Object::View::Default::Html, UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Toolkit.pm100664023532023421 137512544604516 17042 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View =pod =head1 NAME UR::Object::View::Toolkit =head1 SYNOPSIS $v1 = $obj->create_view(toolkit => "gtk"); $v2 = $obj->create_view(toolkit => "tk"); is($v1->_toolkit_delegate, "UR::Object::View::Toolkit::Gtk"); is($v2->_toolkit_delegate, "UR::Object::View::Toolkit::Tk"); =head1 DESCRIPTION Each view delegates to one of these to interact with the toolkit environment =cut package UR::Object::View::Toolkit; use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION;; require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Singleton', is_abstract => 1, has => [ toolkit_name => { is_abstract => 1, is_constant => 1 }, toolkit_module => { is_abstract => 1, is_constant => 1 }, ], ); 1; Text.pm100664023532023421 243012544604516 17757 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Object/View/Toolkitpackage UR::Object::View::Toolkit::Text; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION;; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Toolkit', has => [ toolkit_name => { is_constant => 1, value => "text" }, toolkit_module => { is_constant => 1, value => "(none)" }, # is this used anywhere? ] ); sub show_view { my $class = shift; my $view = shift; my $widget = $view->widget; return $$widget; } # This doesn't really apply for text?! sub hide_view { return undef; my $class = shift; my $view = shift; my $widget = $view->widget; print "DEL: $widget\n"; return 1; } # This doesn't really apply for text?! sub create_window_for_view { return undef; my $class = shift; my $view = shift; my $widget = $view->widget; print "WIN: $widget\n"; return 1; } # This doesn't really apply for text?! sub delete_window_around_view { return undef; my $class = shift; my $widget = shift; print "DEL: $widget\n"; return 1; } 1; =pod =head1 NAME UR::Object::View::Toolkit::Text - Declaration of Text as a View toolkit type =head1 SYNOPSIS Methods called by UR::Object::View to get toolkit specific support for common tasks. =cut ObjectDeprecated.pm100664023532023421 3211612544604516 16501 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Object; # deprecated parts of the UR::Object API use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION; use Data::Dumper; use Scalar::Util qw(blessed); sub get_with_special_parameters { # When overridden, this allows a class to take non-properties as parameters # to get(), and handle loading in a special way. Ideally this is handled by # a custom data source, or properties with smart definitions. my $class = shift; my $rule = shift; Carp::confess( "Unknown parameters to $class get(). " . "Implement get_with_special_parameters() to handle non-standard" . " (non-property) query options.\n" . "The special params were " . Dumper(\@_) . "Rule ID: " . $rule->id . "\n" ); } sub get_or_create { my $self = shift; return $self->get( @_ ) || $self->create( @_ ); } sub set { my $self = shift; my @rvals; while (@_) { my $property_name = shift; my $value = shift; push(@rvals, $self->$property_name($value)); } if(wantarray) { return @rvals; } else { return \@rvals; } } sub property_diff { # Ret hashref of the differences between the object and some other object. # The "other object" may be a hashref or hash, in which case it will # treat each key as a property. my ($self, $other) = @_; my $diff = {}; # If we got a hash instead of a hashref... if (@_ > 2) { shift; $other = { @_ } } no warnings; my $self_value; my $other_value; my $class_object = $self->__meta__; for my $property ($class_object->all_property_names) { if (ref($other) eq 'HASH') { next unless exists $other->{$property}; $other_value = $other->{$property}; } else { $other_value = $other->$property; } $self_value = $self->$property; $diff->{$property} = $self_value if ($other_value ne $self_value); } return $diff; } # TODO: make this a context operation sub unload { my $proto = shift; return unless ($proto->class->__meta__->is_uncachable); my ($self, $class); ref $proto ? $self = $proto : $class = $proto; my $cx = $UR::Context::current; if ( $self ) { # object method # The only things which can be unloaded are things committed to # their database in the exact same state. Everything else must # be reverted or deleted. return unless $self->{db_committed}; if ($self->__changes__) { #warn "NOT UNLOADING CHANGED OBJECT! $self $self->{id}\n"; return; } $self->__signal_change__('unload'); if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM UNLOAD object $self class ",$self->class," id ",$self->id,"\n"; } $cx->_abandon_object($self); return $self; } else { # class method # unload the objects in the class # where there are subclasses of the class # delegate to them my @unloaded; # unload all objects of this class my @involved_classes = ( $class ); for my $obj ($cx->all_objects_loaded_unsubclassed($class)) { push @unloaded, $obj->unload; } # unload any objects that belong to any subclasses for my $subclass ($cx->__meta__->subclasses_loaded($class)) { push @involved_classes, $subclass; push @unloaded, $subclass->unload; } # get rid of the loading info matching this class foreach my $template_id ( keys %$UR::Context::all_params_loaded ) { if (UR::BoolExpr::Template->get($template_id)->subject_class_name->isa($class)) { delete $UR::Context::all_params_loaded->{$template_id}; } } # Turn off the all_objects_are_loaded flags delete @$UR::Context::all_objects_are_loaded{@involved_classes}; return @unloaded; } } # TODO: replace internal calls to go right to the context method sub is_loaded { # this is just here for backward compatability for external calls # get() now goes to the context for data # This shortcut handles the most common case rapidly. # A single ID is passed-in, and the class name used is # not a super class of the specified object. # This logic is in both get() and is_loaded(). my $quit_early = 0; if ( @_ == 2 && !ref($_[1]) ) { unless (defined($_[1])) { Carp::confess(); } my $obj = $UR::Context::all_objects_loaded->{$_[0]}->{$_[1]}; return $obj if $obj; # we could safely return nothing right now, except # that a subclass of this type may have the object return unless $_[0]->__meta__->subclasses_loaded; # nope, there were no subclasses } my $class = shift; my $rule = UR::BoolExpr->resolve_normalized($class,@_); return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); } sub subclasses_loaded { return shift->__meta__->subclasses_loaded(); } # THESE SHOULD PROBABLY GO ON THE CLASS META sub all_objects_are_loaded { # Keep track of which classes claim that they are completely loaded, and that no more loading should be done. # Classes which have the above function return true should set this after actually loading everything. # This class will do just that if it has to load everything itself. my $class = shift; #$meta = $class->__meta__; if (@_) { # Setting the attribute $UR::Context::all_objects_are_loaded->{$class} = shift; } elsif (! exists $UR::Context::all_objects_are_loaded->{$class}) { # unknown... ask the parent classes and remember the answer foreach my $parent_class ( $class->inheritance ) { if (exists $UR::Context::all_objects_are_loaded->{$parent_class}) { $UR::Context::all_objects_are_loaded->{$class} = $UR::Context::all_objects_are_loaded->{$parent_class}; last; } } } return $UR::Context::all_objects_are_loaded->{$class}; } # Observer pattern (old) sub create_subscription { my $self = shift; my %params = @_; # parse parameters my ($class,$id,$method,$callback,$note,$priority); my %translate = ( method => 'aspect', id => 'subject_id', ); my @param_names = qw(method callback note priority id); my %observer_params; for my $name (@param_names) { if (exists $params{$name}) { my $obs_name = $translate{$name} || $name; $observer_params{$obs_name} = delete $params{$name}; } } $observer_params{'subject_class_name'} = $self->class; if (!defined $observer_params{'subject_id'} and ref($self)) { $observer_params{'subject_id'} = $self->id; } if (my @unknown = keys %params) { Carp::croak "Unknown options @unknown passed to create_subscription!"; } # validate if (my @bad_params = %params) { Carp::croak "Bad params passed to add_listener: @bad_params"; } my $observer = UR::Observer->create(%observer_params); return unless $observer; return [@observer_params{'subject_class_name','subject_id','aspect','callback','note'}]; } sub validate_subscription { # Everything is invalid unless you make it valid by implementing # validate_subscription on your class. (Or use the new API.) return; } sub inform_subscription_cancellation { # This can be overridden in derived classes if the class wants to know # when subscriptions are cancelled. return 1; } sub cancel_change_subscription ($@) { my ($class,$id,$property,$callback,$note); if (@_ >= 4) { ($class,$id,$property,$callback,$note) = @_; die "Bad parameters." if ref($class); } elsif ( (@_==3) or (@_==2) ) { ($class, $property, $callback) = @_; if (ref($_[0])) { $class = ref($_[0]); $id = $_[0]->id; } } else { die "Bad parameters."; } my %params; if (defined $class) { $params{'subject_class_name'} = $class; } if (defined $id) { $params{'subject_id'} = $id; } if (defined $property) { $params{'aspect'} = $property; } if (defined $callback) { $params{'callback'} = $callback; } if (defined $note) { $params{'note'} = $note; } my @observers = UR::Observer->get(%params); return unless @observers; if (@observers > 1) { Carp::croak('Matched more than one observer within cancel_change_subscription(). Params were: ' . join(', ', map { "$_ => " . $params{$_} } keys %params)); } $observers[0]->delete(); } # This should go away when we shift to fully to a transaction log for deletions. sub ghost_class { my $class = $_[0]->class; $class = $class . '::Ghost'; return $class; } package UR::ModuleBase; # Method for setting a callback using the old, non-command messaging API =pod =over 4 =item message_callback $sub_ref = UR::ModuleBase->message_callback($type); UR::ModuleBase->message_callback($type, $sub_ref); This method returns and optionally sets the subroutine that handles messages of a specific type. =back =cut ## set or return a callback that has been created for a message type sub message_callback { my $self = shift; my ($type, $callback) = @_; my $methodname = $type . '_messages_callback'; if (!$callback) { # to clear the old, deprecated non-command messaging API callback return UR::Object->$methodname($callback); } my $wrapper_callback = sub { my($obj,$msg) = @_; my $obj_class = $obj->class; my $obj_id = (ref($obj) ? ($obj->can("id") ? $obj->id : $obj) : $obj); my $message_package = $type . '_package'; my $message_object = UR::ModuleBase::Message->create ( text => $msg, level => 1, package_name => $obj->$message_package(), call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); $callback->($message_object, $obj, $type); $_[1] = $message_object->text; }; # To support the old, deprecated, non-command messaging API UR::Object->$methodname($wrapper_callback); } sub message_object { my $self = shift; # see how we were called if (@_ < 2) { no strict 'refs'; # return the message object my ($type) = @_; my $method = $type . '_message'; my $msg_text = $self->method(); my $obj_class = $self->class; my $obj_id = (ref($self) ? ($self->can("id") ? $self->id : $self) : $self); my $msgdata = $self->_get_msgdata(); return UR::ModuleBase::Message->create ( text => $msg_text, level => 1, package_name => $msgdata->{$type . '_package'}, call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); } } foreach my $type ( UR::ModuleBase->message_types ) { my $retriever_name = $type . '_text'; my $compat_name = $type . '_message'; my $sub = sub { my $self = shift; return $self->$compat_name(); }; no strict 'refs'; *$retriever_name = $sub; } # class that stores and manages messages for the deprecated API package UR::ModuleBase::Message; use Scalar::Util qw(weaken); ##- use UR::Util; UR::Util->generate_readonly_methods ( text => undef, level => undef, package_name => undef, call_stack => [], time_stamp => undef, owner_class => undef, owner_id => undef, type => undef, ); sub create { my $class = shift; my $obj = {@_}; bless ($obj,$class); weaken $obj->{'owner_id'} if (ref($obj->{'owner_id'})); return $obj; } sub owner { my $self = shift; my ($owner_class,$owner_id) = ($self->owner_class, $self->owner_id); if (not defined($owner_id)) { return $owner_class; } elsif (ref($owner_id)) { return $owner_id; } else { return $owner_class->get($owner_id); } } sub string { my $self = shift; "$self->{time_stamp} $self->{type}: $self->{text}\n"; } sub _stack_item_params { my ($self, $stack_item) = @_; my ($function, $parameters, @parameters); return unless ($stack_item =~ s/\) called at [^\)]+ line [^\)]+\s*$/\)/); if ($stack_item =~ /^\s*([^\(]*)(.*)$/) { $function = $1; $parameters = $2; @parameters = eval $parameters; return ($function, @parameters); } else { return; } } package UR::Object; 1; ObjectV001removed.pm100664023532023421 672012544604516 16433 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Object; =pod =head1 NAME UR::ObjectV001removed - restores changes removed in UR version 0.01 =head1 SYNOPSIS use UR::ObjectV001removed =head1 DESCRIPTION Extends the UR::Object API have methods removed in the 0.01 release. If you upgrade UR, but depend on old APIs, use this module. For version 0.xx of UR, APIs may change with each release. After 1.0, APIs will only change with major releases number increments. =cut use warnings; use strict; our $VERSION = "0.44"; # UR $VERSION; use Data::Dumper; use Scalar::Util qw(blessed); *get_class_meta = sub { shift->__meta__ }; *get_class_object = sub { shift->__meta__ }; *get_rule_for_params = \&define_boolexpr; *get_boolexpr_for_params = \&define_boolexpr; *get_object_set = \&define_set; our ($all_objects_loaded, $all_change_subscriptions, $all_objects_are_loaded, $all_params_loaded); *all_objects_loaded = \$UR::Context::all_objects_loaded; *all_change_subscriptions = \$UR::Context::all_change_subscriptions; *all_objects_are_loaded = \$UR::Context::all_objects_are_loaded; *all_params_loaded = \$UR::Context::all_params_loaded; # These live in UR::Context, where they may switch to point to # different data structures depending on sub-context, transaction, etc. # They are aliased here for backward compatability, since many parts # of the system use $UR::Object::whatever to work with them directly. sub load { # this is here for backward external compatability # get() now goes directly to the context my $class = shift; if (ref $class) { # Trying to reload a specific object? if (@_) { Carp::confess("load() on an instance with parameters is not supported"); return; } @_ = ('id' ,$class->id()); $class = ref $class; } my ($rule, @extra) = UR::BoolExpr->resolve_normalized($class,@_); if (@extra) { if (scalar @extra == 2 and $extra[0] eq "sql") { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } else { die "Odd parameters passed directly to $class load(): @extra.\n" . "Processable params were: " . Data::Dumper::Dumper({ $rule->params_list }); } } return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } sub _load { Carp::cluck(); my ($class,$rule) = @_; return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } sub dbh { Carp::confess("Attempt to call dbh() on a UR::Object.\n" . "Objects no longer have DB handles, data_sources do\n" . "use resolve_data_sources_for_class_meta_and_rule() on a UR::Context instead"); my $ds = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule(shift->__meta__); return $ds->get_default_handle; } sub matches { no warnings; my $self = shift; my %param = $self->preprocess_params(@_); for my $key (keys %param) { next unless $self->can($key); return 0 unless $self->$key eq $param{$key} } return 1; } sub property_names { my $class = shift; my $meta = $class->__meta__; return $meta->all_property_names; } sub _is_loaded { Carp::cluck(); my ($class,$rule) = @_; return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); } # as we remove more logic from the default API, add extensions here. use UR::ObjectV04removed; ObjectV04removed.pm100664023532023421 311512544604516 16351 0ustar00abrummetgsc000000000000UR-0.44/lib/UR=pod =head1 NAME UR::ObjectV04removed - restores changes removed in UR version 0.04 =head1 SYNOPSIS use UR::ObjectV04removed =head1 DESCRIPTION Extends the UR::Object API have methods removed in the 0.04 release. If you upgrade UR, but depend on old APIs, use this module. For version 0.xx of UR, APIs may change with each release. After 1.0, APIs will only change with major releases number increments. =cut # version 0.4 commits significant refactoring of the UR::BoolExpr API # this brings back those parts which got new names package UR::BoolExpr; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; *get_rule_template = \&template; *rule_template = \&template; *get_rule_template_and_values = \&template_and_values; *get_template_and_values = \&template_and_values; *get_values = \&values; *get_underlying_rules = \&underlying_rules; *specifies_value_for_property_name = \&specifies_value_for; *specified_operator_for = \&operator_for; *specified_operator_for_propety_name = \&operator_for; *specified_value_for_id = \&value_for_id; *specified_value_for_position = \&value_for_position; *specified_value_for_property_name = \&value_for; *create_from_filter_string = \&resolve_for_string; *create_from_command_line_format_filters = \&_resolve_from_filter_array; *create_from_filters = \&_resolve_from_filter_array; *create_from_subject_class_name_keys_and_values = \&_resolve_from_subject_class_name_keys_and_values; *resolve_normalized_rule_for_class_and_params = \&resolve_normalized; *resolve_for_class_and_params = \&resolve; *get_normalized_rule_equivalent = \&normalize; Observer.pm100664023532023421 3436612544604516 15112 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Observer; use strict; use warnings; BEGIN { require UR; require UR::Context::Transaction; }; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, has => [ subject_class_name => { is => 'Text', is_optional => 1, default_value => 'UR::Object' }, subject_id => { is => 'SCALAR', is_optional => 1, default_value => '' }, aspect => { is => 'String', is_optional => 1, default_value => '' }, priority => { is => 'Number', is_optional => 1, default_value => 1 }, note => { is => 'String', is_optional => 1, default_value => '' }, once => { is => 'Boolean', is_optional => 1, default_value => 0 }, subject => { is => 'UR::Object', id_by => 'subject_id', id_class_by => 'subject_class_name' }, ], is_transactional => 1, ); # This is not implemented as a "real" observer via create() because at the point during bootstrapping # that this module is loaded, we're not yet ready to start creating objects __PACKAGE__->_insert_record_into_all_change_subscriptions('UR::Observer', 'priority', '', [\&_modify_priority, '', 0, UR::Object::Type->autogenerate_new_object_id_uuid]); sub create { my $class = shift; $class->_create_or_define('create', @_); } sub __define__ { my $class = shift; $class->_create_or_define('__define__', @_); } my @required_params_for_register = qw(aspect callback note once priority subject_class_name subject_id id); sub required_params_for_register { @required_params_for_register } sub _create_or_define { my $class = shift; my $method = shift; my %params = @_; my $callback = delete $params{callback}; my $self = UR::Context::Transaction::do { my $inner_self; if ($method eq 'create') { $inner_self = $class->SUPER::create(%params); } elsif ($method eq '__define__') { $inner_self = $class->SUPER::__define__(%params); } else { Carp::croak('Instantiating a UR::Observer with some method other than create() or __define__() is not supported'); } $inner_self->{callback} = $callback; $inner_self->register_callback(map { $_ => $inner_self->$_ } @required_params_for_register); return $inner_self; }; return $self; } { my @has_defaults = qw(aspect note once priority subject_class_name subject_id); sub has_defaults { @has_defaults } my %defaults = map { $_ => __PACKAGE__->__meta__->{has}->{$_}->{default_value} } grep { exists __PACKAGE__->__meta__->{has}->{$_} && exists __PACKAGE__->__meta__->{has}->{$_}->{default_value} } @has_defaults; sub defaults_for_register_callback { %defaults } } sub register_callback { my $class = shift; my %params = @_; unless (defined $params{id}) { $params{id} = UR::Object::Type->autogenerate_new_object_id_uuid; } my %values = $class->defaults_for_register_callback(); my @specified_params = grep { exists $params{$_} } @required_params_for_register; @values{@specified_params} = map { delete $params{$_} } @specified_params; my @bad_params = keys %params; if (@bad_params) { Carp::croak('invalid params: ' . join(', ', @bad_params)); } my @missing_params = grep { not exists $values{$_} } @required_params_for_register; if (@missing_params) { Carp::croak('missing required params: ' . join(', ', @missing_params)); } my @undef_values = grep { not defined $values{$_} } keys %values; if (@undef_values) { Carp::croak('undefined values: ' . join(', ', @undef_values)); } my $subject_class_name = $values{subject_class_name}; my $subject_class_meta = eval { $subject_class_name->__meta__ }; if ($@) { Carp::croak("Can't create observer with subject_class_name '$subject_class_name': Can't get class metadata for class '$subject_class_name': $@"); } unless ($subject_class_meta) { Carp::croak("Class $subject_class_name cannot be the subject class for an observer because there is no class metadata"); } my $aspect = $values{aspect}; my $subject_id = $values{subject_id}; unless ($subject_class_meta->_is_valid_signal($aspect)) { my $croak = sub { Carp::croak("'$aspect' is not a valid aspect for class $subject_class_name") }; unless ($subject_class_name->can('validate_subscription')) { $croak->(); } unless ($subject_class_name->validate_subscription($aspect, $subject_id, $values{callback})) { $croak->(); } } $class->_insert_record_into_all_change_subscriptions( @values{qw(subject_class_name aspect subject_id)}, [@values{qw(callback note priority id once)}], ); return $values{id}; } sub _insert_record_into_all_change_subscriptions { my($class,$subject_class_name, $aspect,$subject_id, $new_record) = @_; if ($subject_class_name eq 'UR::Object') { $subject_class_name = ''; }; my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id} ||= []; push @$list, $new_record; } sub _modify_priority { my($self, $aspect, $old_val, $new_val) = @_; my $subject_class_name = $self->subject_class_name; my $subject_aspect = $self->aspect; my $subject_id = $self->subject_id; my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$subject_aspect}->{$subject_id}; return unless $list; # this is probably an error condition my $data; for (my $i = 0; $i < @$list; $i++) { if ($list->[$i]->[3] eq $self->id) { ($data) = splice(@$list,$i, 1); last; } } return unless $data; # This is probably an error condition... $data->[2] = $new_val; $self->_insert_record_into_all_change_subscriptions($subject_class_name, $subject_aspect, $subject_id, $data); } sub callback { shift->{callback}; } sub subscription { shift->{subscription} } sub unregister_callback { my $class = shift; my %params = @_; my $id = delete $params{id}; unless (defined $id) { Carp::croak('missing required parameter: id'); } my @undef_params = grep { not defined $params{$_} } keys %params; if (@undef_params) { Carp::croak('undefined params: ' . join(', ', @undef_params)); } my $aspect = delete $params{aspect}; my $subject_class_name = delete $params{subject_class_name}; my $subject_id = delete $params{subject_id}; my @bad_params = keys %params; if (@bad_params) { Carp::croak('invalid params: ' . join(', ', @bad_params)); } my @subject_class_names = $subject_class_name || keys %{$UR::Context::all_change_subscriptions}; for my $subject_class_name (@subject_class_names) { my @aspects = $aspect || keys %{$UR::Context::all_change_subscriptions->{$subject_class_name}}; for my $aspect (@aspects) { my @subject_ids = $subject_id || keys %{$UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}}; for my $subject_id (@subject_ids) { my $arrayref = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id}; for (my $i = 0; $i < @$arrayref; $i++) { if ($arrayref->[$i]->[3] eq $id) { splice(@$arrayref, $i, 1); if (@$arrayref == 0) { $arrayref = undef; delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id}; if (not keys %{ $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect} }) { delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}; } } return 1; } } } } } return; } sub delete { my $self = shift; #$DB::single = 1; my $subject_class_name = $self->subject_class_name; my $subject_id = $self->subject_id; my $aspect = $self->aspect; $subject_class_name = '' if (! $subject_class_name or $subject_class_name eq 'UR::Object'); $subject_id = '' unless (defined $subject_id); $aspect = '' unless (defined $aspect); my $unregistered = $self->unregister_callback( aspect => $aspect, id => $self->id, subject_class_name => $subject_class_name, subject_id => $subject_id, ); if ($unregistered) { unless ($subject_class_name eq '' || $subject_class_name->inform_subscription_cancellation($aspect, $subject_id, $self->{'callback'})) { Carp::confess("Failed to validate requested subscription cancellation for aspect '$aspect' on class $subject_class_name"); } } $self->SUPER::delete(); } sub __rollback__ { my $self = shift; return UR::Observer::delete($self); } sub get_with_special_parameters { my($class,$rule,%extra) = @_; my $callback = delete $extra{'callback'}; if (keys %extra) { Carp::croak("Unrecognized parameters in get(): " . join(', ', keys(%extra))); } my @matches = $class->get($rule); return grep { $_->callback eq $callback } @matches; } 1; =pod =head1 NAME UR::Observer - bind callbacks to object changes =head1 SYNOPSIS $rocket = Acme::Rocket->create( fuel_level => 100 ); $observer = $rocket->add_observer( aspect => 'fuel_level', callback => sub { print "fuel level is: " . shift->fuel_level . "\n" }, priority => 2, ); $observer2 = UR::Observer->create( subject_class_name => 'Acme::Rocket', subject_id => $rocket->id, aspect => 'fuel_level', callback => sub { my($self,$changed_aspect,$old_value,$new_value) = @_; if ($new_value == 0) { print "Bail out!\n"; } }, priority => 0 ); for (3 .. 0) { $rocket->fuel_level($_); } # fuel level is: 3 # fuel level is: 2 # fuel level is: 1 # Bail out! # fuel level is: 0 $observer->delete; =head1 DESCRIPTION UR::Observer implements the observer pattern for UR objects. These observers can be attached to individual object instances, or to whole classes. They can send notifications for changes to object attributes, or to other state changes such as when an object is loaded from its datasource or deleted. =head1 CONSTRUCTOR Observers can be created either by using the method C on another class, or by calling C on the UR::Observer class. my $o1 = Some::Other::Class->add_observer(...); my $o2 = $object_instance->add_observer(...); my $o3 = UR::Observer->create(...); The constructor accepts these parameters: =over 2 =item subject_class_name The name of the class the observer is watching. If this observer is being created via C, then it figures out the subject_class_name from the class or object it is being called on. =item subject_id The ID of the object the observer is watching. If this observer is being created via C, then it figures out the subject_id from the object it was called on. If C was called as a class method, then subject_id is omitted, and means that the observer should fire for changes on any instance of the class or sub-class. =item priority A numeric value used to determine the order the callbacks are fired. Lower numbers are higher priority, and are run before callbacks with a numerically higher priority. The default priority is 1. Negative numbers are ok. =item aspect The attribute the observer is watching for changes on. The aspect is commonly one of the properties of the class. In this case, the callback is fired after the property's value changes. aspect can be omitted, which means the observer should fire for any change in the object state. If both subject_id and aspect are omitted, then the observer will fire for any change to any instance of the class. There are other, system-level aspects that can be watched for that correspond to other types of state change: =over 2 =item create After a new object instance is created =item delete After an n object instance is deleted =item load After an object instance is loaded from its data source =item commit After an object instance has changes saved to its data source =back =item callback A coderef that is called after the observer's event happens. The coderef is passed four parameters: $self, $aspect, $old_value, $new_value. In this case, $self is the object that is changing, not the UR::Observer instance (unless, of course, you have created an observer on UR::Observer). The return value of the callback is ignored. =item once If the 'once' attribute is true, the observer is deleted immediately after the callback is run. This has the effect of running the callback only once, no matter how many times the observer condition is triggered. =item note A text string that is ignored by the system =back =head2 Custom aspects You can create an observer for an aspect that is neither a property nor one of the system aspects by listing the aspect names in the metadata for the class. class My::Class { has => [ 'prop_a', 'another_prop' ], valid_signals => ['custom', 'pow' ], }; my $o = My::Class->add_observer( aspect => 'pow', callback => sub { print "POW!\n" }, ); My::Class->__signal_observers__('pow'); # POW! my $obj = My::Class->create(prop_a => 1); $obj->__signal_observers__('custom'); # not an error To help catch typos, creating an observer for a non-standard aspect throws an exception unless the named aspect is in the list of 'valid_signals' in the class metadata. Nothing in the system will trigger these observers, but they can be triggered in your own code using the C<__signal_observers()__> class or object method. Sending a signal for an aspect that no observers are watching for is not an error. =cut JsonRpcServer.pm100664023532023421 2634512544604516 17466 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Servicepackage UR::Service::JsonRpcServer; use strict; use warnings; use UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object', properties => [ host => { type => 'String', is_transient => 1, default_value => '0.0.0.0', doc => 'The local address to listen on'}, port => { type => 'String', is_transient => 1, default_value => 8080, doc => 'The local port to listen on'}, server => { type => 'Net::HTTPServer', is_transient => 1, doc => 'The Net::HTTPServer instance for this Server instance' }, api_root => { type => 'String', is_transient => 1, default_value => 'URapi' }, ], id_by => ['host','port'], doc => 'An object serving as a web server to respond to JSON-RPC requests; wraps Net::HTTPServer', ); =pod =head1 NAME UR::Service::JsonRpcServer - A self-contained JSON-RPC server for UR namespaces =head1 SYNOPSIS use lib '/path/to/your/moduletree'; use YourNamespace; my $rpc = UR::Service::JsonRpcServer->create(host => 'localhost', port => '8080', api_root => 'URapi', docroot => '/html/pages/path', ); $rpc->process(); =head1 Description This is a class containing an implementation of a JSON-RPC server to respond to requests involving UR-based namespaces and their objects. It uses Net::HTTPServer as the web server back-end library. Incoming requests are divided into two major categories: =over 4 =item http://server:port/C/class/Namespace/Class This is the URL for a call to a class metnod on C =item http://server:port/C/obj/Namespace/Class/id This is the URL for a method call on an object of class Namespace::Class with the given id =back =head1 Constructor The constructor takes the following named parameters: =over 4 =item host The hostname to listen on. This can be an ip address, host name, or undef. The default value is '0.0.0.0'. This argument is passed along verbatim to the Net::HTTPServer constructor. =item port The TCP port to listen on. The default value is 8080. This argument is passed along verbatim to the Net::HTTPServer constructor. =item api_root The root path that the http server will listen for requests on. The constructor registers two paths with the Net::HTTPServer with RegisterRegex() for /C/class/* and /C/obj/* to respond to class and instance metod calls. =back All other arguments are passed along to the Net::HTTPServer constructor. =head1 Methods =over 4 =item $rpc->process() A wrapper to the Net::HTTPServer Process() method. With no arguments, this call will block forever from the perspective of the caller, and process all http requests coming in. You can optionally pass in a timeout value in seconds, and it will respond to requests for the given number of seconds before returning. =back =head1 Client Side There are (or will be) client-side code in both Perl and Javascript. The Perl code is (will be) implemented as a UR::Context layer that will return light-weight object instances containing only class info and IDs. All method calls will be serialized and sent over the wire for the server process to execute them. The Javascript interface is defined in the file urinterface.js. An example: var UR = new URInterface('http://localhost:8080/URApi/'); // Connect to the server var FooThingy = UR.get_class('Foo::Thingy'); // Get the class object for Foo::Thingy var thingy = FooThingy.get(1234); // Retrieve an instance with ID 1234 var result = thingy.call('method_name', 1, 2, 3); // Call $thingy->method_name(1,2,3) on the server =head1 SEE ALSO Ney::HTTPServer, urinterface.js =cut use Net::HTTPServer; use JSON; use Class::Inspector; sub create { my($class,%args) = @_; my $api_root = delete $args{'api_root'}; my $server = Net::HTTPServer->new(%args); return unless $server; my %create_args = ( host => $args{'host'}, port => $args{'port'} ); $create_args{'api_root'} = $api_root if defined $api_root; my $self = $class->SUPER::create(%create_args); return unless $self; $self->server($server); $server->RegisterRegex("^/$api_root/class/*", sub { $self->_api_entry_classes(@_) } ) if $api_root; $server->RegisterRegex("^/$api_root/obj/*", sub { $self->_api_entry_obj(@_) } ) if $api_root; my $port = $server->Start(); if ($args{'port'} eq 'scan') { $self->port($port); } return $self; } sub process { my $self = shift; #$self->server->Process(@_); my $server = $self->server; $server->Process(@_); } sub _api_entry_classes { my($self,$request) = @_; my $response = $request->Response(); #$DB::single = 1; my $data = $self->_get_post_data_from_request($request); #my $struct = decode_json($data); my $struct = jsonToObj($data); my $class = $self->_parse_class_from_request($request); unless ($class) { $response->Code(404); $response->Print("Couldn't parse URL " . $request->URL); return $response; } my $method = $struct->{'method'}; my $params = $struct->{'params'}; my @retval; if ($method eq '_get_class_info') { # called when the other end gets a class object eval { my $class_object = $class->__meta__; my %id_names = map { $_ => 1 } $class_object->all_id_property_names(); my @id_names = keys(%id_names); my %property_names = map { $_ => 1 } grep { ! exists $id_names{$_} } $class_object->all_property_names(); my @property_names = keys(%property_names); my $possible_method_names = Class::Inspector->methods($class, 'public'); my @method_names = grep { ! exists $id_names{$_} and ! exists $property_names{$_} } @$possible_method_names; push @retval, { id_properties => \@id_names, properties => \@property_names, methods => \@method_names }; }; } else { eval { @retval = $class->$method(@$params); }; } my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}, result => \@retval}; if ($@) { $return_struct->{'result'} = undef; $return_struct->{'error'} = $@; } else { foreach my $item ( @retval ) { my $reftype = ref $item; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { # If it's an object of some sort my %copy = %$item; $copy{'object_type'} = $class; $item = \%copy; } } $return_struct->{'result'} = \@retval; } #my $encoded_result = to_json($return_struct, {convert_blessed => 1}); my $encoded_result = objToJson($return_struct); $response->Print($encoded_result); return $response; } sub _api_entry_obj { my($self,$request) = @_; my $response = $request->Response(); #$DB::single = 1; my $data = $self->_get_post_data_from_request($request); #my $struct = decode_json($data); my $struct = jsonToObj($data); my($class,$id) = $self->_parse_class_and_id_from_request($request); unless ($class) { $response->Code(404); $response->Print("Couldn't parse URL " . $request->URL); return $response; } my $method = $struct->{'method'}; my $params = $struct->{'params'}; my @retval; eval { my $obj = $class->get($id); @retval = $obj->$method(@$params); }; my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}}; if ($@) { $return_struct->{'result'} = undef; $return_struct->{'error'} = $@; } else { foreach my $item ( @retval ) { my $reftype = ref $item; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { # If it's an object of some sort my %copy = %$item; $copy{'object_type'} = $class; $item = \%copy; } } $return_struct->{'result'} = \@retval; } #my $encoded_result = to_json($return_struct, {convert_blessed => 1}); my $encoded_result = objToJson($return_struct); $response->Print($encoded_result); return $response; } ## This one uses the last part of the URL as the ID - won't work with a generic get() #sub old_api_entry_point { # my($self,$request) = @_; # # my $response = $request->Response(); # ##$DB::single = 1; # my $data = $self->_get_post_data_from_request($request); # my $struct = decode_json($data); # # my($class,$id) = $self->_parse_class_and_id_from_request($request); # unless ($class) { # $response->Code(404); # $response->Print("Couldn't parse URL " . $request->URL); # return $response; # } # # my $method = $struct->{'method'}; # my $params = $struct->{'params'}; # my @retval; # eval { # my $obj = $class->get($id); # if ($method eq 'get') { # my %copy = %$obj; # $retval[0] = \%copy; # } else { # @retval = $obj->$method(@$params); # } # }; # # my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}}; # if ($@) { # $return_struct->{'result'} = undef; # $return_struct->{'error'} = $@; # } else { # $return_struct->{'result'} = \@retval; # } # # # my $encoded_result = to_json($return_struct, {convert_blessed => 1}); # $response->Print($encoded_result); # # return $response; #} # URLs are expected to look something like this: # http://server/URapi/Namespace/Class/Name/ID # and would translate to the class Namespace::Class::Name with the ID property ID sub _parse_class_and_id_from_request { my($self,$request) = @_; my $api_root = $self->api_root; my $url = $request->URL(); my @api_root = split(/\//,$api_root); my @url_parts = split(/\//, $url); shift @url_parts until ($url_parts[0]); { no warnings 'uninitialized'; while($api_root[0] eq $url_parts[0]) { shift @api_root; shift @url_parts; } } shift @url_parts if ($url_parts[0] eq 'class' || $url_parts[0] eq 'obj'); my $id = pop @url_parts; my $class = join('::', @url_parts); return($class,$id); } # This works for URLs that don't have an ID at the end sub _parse_class_from_request { my($self,$request) = @_; my $api_root = $self->api_root; my $url = $request->URL(); my @api_root = split(/\//,$api_root); my @url_parts = split(/\//, $url); shift @url_parts until ($url_parts[0]); { no warnings 'uninitialized'; while($api_root[0] eq $url_parts[0]) { shift @api_root; shift @url_parts; } } shift @url_parts if ($url_parts[0] eq 'class' || $url_parts[0] eq 'obj'); my $class = join('::', @url_parts); return $class; } sub _get_post_data_from_request { my($self,$request) = @_; my $message = $request->Request; my($data) = ($message =~ m/\r\n\r\n(.*)/m); return $data; } 1; Executer.pm100664023532023421 1252412544604516 17123 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/RPCpackage UR::Service::RPC::Executer; use UR; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; class UR::Service::RPC::Executer { has => [ fh => { is => 'IO::Handle', doc => 'handle we will send and receive messages across' }, ], has_optional => [ use_sigio => { is => 'Boolean', default_value => 0 }, ], is_transactional => 0, }; sub create { my $class = shift; my $obj = $class->SUPER::create(@_); return unless $obj; if ($obj->use_sigio) { UR::Service::RPC->enable_sigio_processing($obj); } $obj->create_subscription(method => 'use_sigio', callback => sub { my ($changed_object, $changed_property, $old_value, $new_value) = @_; return 1 if ($old_value == $new_value); if ($new_value) { UR::Service::RPC->enable_sigio_processing($obj); } else { UR::Service::RPC->disable_sigio_processing($obj); } }); return $obj; } # sub classes can override this # If they're going to reject the request, $msg should be modified in place # with a return value and exception, because we'lre going to return it right back # to the requester sub authenticate { # my($self,$msg) = @_; return 1; } # Process one message off of the file handle sub execute { my $self = shift; my $msg = UR::Service::RPC::Message->recv($self->fh); unless ($msg) { # The other end probably closed the socket $self->close_connection(); return 1; } my $response; if ($self->authenticate($msg)) { my $target_class = $msg->target_class || ref($self); my $method = $msg->method_name; my @arglist = $msg->param_list; my $wantarray = $msg->wantarray; my %resp_msg_args = ( target_class => $target_class, method_name => $method, params => \@arglist, 'wantarray' => $wantarray, fh => $self->fh ); my $method_name = join('::',$target_class, $method); if ($wantarray) { my @retval; eval { no strict 'refs'; @retval = &{$method_name}(@arglist); }; $resp_msg_args{'return_values'} = \@retval unless ($@); } elsif (defined $wantarray) { my $retval; eval { no strict 'refs'; no warnings; $retval = &{$method_name}(@arglist); }; $resp_msg_args{'return_values'} = [$retval] unless ($@); } else { eval { no strict 'refs'; &{$method_name}(@arglist); }; } $resp_msg_args{'exception'} = $@ if $@; $response = UR::Service::RPC::Message->create(%resp_msg_args); } else { # didn't authenticate. $response = $msg; } unless ($response->send()) { $self->fh->close(); } return 1; } sub close_connection { my $self = shift; $self->use_sigio(0); $self->fh->close(); } 1; =pod =head1 NAME UR::Service::RPC::Executer - Base class for modules implementing RPC executers =head1 DESCRIPTION This class is an abstract base class used to implement RPC executers. That is, modules meant to have their methods called from another process, and have the results passed back to the original caller. The communication happens over a read-write filehandle such as a socket by passing L objects back and forth. Executors are subordinate to a L object which handles decoding the message passed over the socket, calling the method on the correct executor in the right context, and returning the result back through the file handle. =head1 PROPERTIES =over 4 =item fh => IO::Handle File handle messages are received on and responses are sent to =item use_sigio => Boolean If true, the Server will set up a callback on the IO signal to handle execution, so the Server does not need to block in loop(). =back =head1 METHODS =over 4 =item authenticate $bool = $exec->authenticate($msg); This is called by execute() after the message object is deserialized from the filehandle. The default implementation just returns true. Subclasses can override this to examine the UR::Service::RPC::Message object and return true or fale whether it should allow or disallow execution. If authentication fails, the Executor should modify the Message object in-place with a proper return value and exception. =item execute $exec->execute(); Called when the Server detects data is available to read on its file handle. It deserializes the message and calls authenticate. If authentication fails, it immediately passes the message object back to the caller. If authentication succeedes, it calls the appropriate method in the Executor package, and creates a new Message object with the return value to pass back to the caller. =item close_connection $exec->close_connection(); Called by execute() when it detects that the file handle has closed. =back Derived classes should define additional methods that then become callable by execute(). =head1 SEE ALSO UR::Service::RPC::Server, UR::Service::RPC::Message =cut Message.pm100664023532023421 1266712544604516 16733 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/RPCpackage UR::Service::RPC::Message; use UR; use FreezeThaw; use IO::Select; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Service::RPC::Message', has => [ target_class => { is => 'String' }, method_name => { is => 'String' }, ], has_optional => [ #arg_list => { is => 'ARRAY' }, params => { is => 'Object', is_many => 1 }, return_values => { is => 'Object', is_many => 1 }, 'wantarray' => { is => 'Integer' }, fh => { is => 'IO::Handle' }, exception => { is => 'String' }, ], is_transactional => 0, ); sub create { my($class,%params) = @_; foreach my $key ( 'params', 'return_values' ) { if (!$params{$key}) { $params{$key} = []; } elsif (ref($params{$key}) ne 'ARRAY') { $params{$key} = [ $params{$key} ]; } } return $class->SUPER::create(%params); } sub send { my $self = shift; my $fh = shift; $fh ||= $self->fh; my %struct; foreach my $key ( qw (target_class method_name params wantarray return_values exception) ) { $struct{$key} = $self->{$key}; } my $string = FreezeThaw::freeze(\%struct); $string = pack('N', length($string)) . $string; my $len = length($string); my $sent = 0; while($sent < $len) { my $wrote = $fh->syswrite($string, $len - $sent, $sent); if ($wrote) { $sent += $wrote; } else { # The filehandle closed for some reason $fh->close; return undef; } } return $sent; } sub recv { my($class, $fh, $timeout) = @_; # You can also call recv on a message object previously created if (ref($class) && $class->isa('UR::Service::RPC::Message')) { my $fh = $class->fh; $class = ref($class); return $class->recv($fh); } if (@_ < 3) { # # if they didn't specify a timeout $timeout = 5; # Default wait 5 sec } my $select = IO::Select->new($fh); # read in the message len, 4 chars my $msglen; my $numchars = 0; while ($numchars < 4) { unless ($select->can_read($timeout)) { $class->warning_message("Can't get message length, timed out"); return; } my $read = $fh->sysread($msglen, 4-$numchars, $numchars); unless ($read) { $class->warning_message("Can't get message length: $!"); return; } $numchars += $read; } $msglen = unpack('N', $msglen); my $string = ''; $numchars = 0; while ($numchars < $msglen) { unless ($select->can_read($timeout)) { $class->warning_message("Timed out reading message after $numchars bytes"); return; } my $read = $fh->sysread($string, $msglen - $numchars, $numchars); unless($read) { $class->warning_message("Error reading message after $numchars bytes: $!"); return; } $numchars += $read; } my($struct) = FreezeThaw::thaw($string); my $obj = $class->create(%$struct, fh => $fh); return $obj; } 1; =pod =head1 NAME UR::Service::RPC::Message - Serializable object appropriate for sending RPC messages =head1 SYNOPSIS my $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); $msg->send($fh); my $resp = UR::Service::RPC::Message->recv($fh, 5); =head1 DESCRIPTION This class is used as a message-passing interface by the RPC service modules. =head1 PROPERTIES These properties should be filled in by the initiating caller =over 4 =item method_name => Text The name of the subroutine the initiator whishes to call. =item target_class => Text The namespace the initiator wants the subroutine to be called in =item params => ARRAY List of parameters to pass to the subroutine =item wantarray => Boolean What wantarray() context the subroutine should be called in. =back These properties are assigned after the RPC call to the subroutine =over 4 =item return_values => ARRAY List of values returned by the subroutine =item exception On the receiving side, the subroutine is called within an eval. If there was an exception, C stores the value of $@, or the empty string. The receiving side should also fill-in C if there was an authentication failure. =item fh C fills this in with the file handle the message was read from. =back =head1 METHODS =over 4 =item send $bytes = $msg->send($fh); Serializes the Message object with FreezeThaw and writes the data to the filehandle $fh. Returns the number of bytes written. $bytes will be false if there was an error. =item recv $response = UR::Service::RPC::Message->recv($fh,$timeout); $response = $msg->recv(); Reads a serialized Message from the filehandle and constructs a Message object that is then returned to the caller. In the first case, it reads from the given filehandle, waiting a maximum of $timeout seconds with select before giving up. In the second case, it reads from whatever filehandle is stored in $msg to read data from. =back =head1 SEE ALSO UR::Service::RPC::Server, UR::Service::RPC::Executor =cut Server.pm100664023532023421 602412544604516 16563 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/RPCpackage UR::Service::RPC::Server; use UR; use IO::Select; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; # We're going to be essentially reimplementing an Event queue here. :( class UR::Service::RPC::Server { has => [ 'select' => { is => 'IO::Select' }, timeout => { is => 'Float', default_value => undef }, executers => { is => 'HASH', doc => 'maps file handles to the UR::Service::RPC::Executer objects we are working with' }, ], }; sub create { my($class, %args) = @_; unless ($args{'executers'}) { $args{'executers'} = {}; } unless ($args{'select'}) { my @fh = map { $_->fh } values %{$args{'executers'}}; $args{'select'} = IO::Select->new(@fh); } my $self = $class->SUPER::create(%args); return $self; } sub add_executer { my($self,$executer,$fh) = @_; unless ($fh) { if ($executer->can('fh')) { $fh = $executer->fh; } else { $self->error_message("Cannot determine file handle for RPC executer $executer"); return; } } $self->{'executers'}->{$fh} = $executer; $self->select->add($fh); } sub loop { my $self = shift; my $timeout; if (@_) { $timeout = shift; } else { $timeout = $self->timeout; } my @ready = $self->select->can_read($timeout); my $count = 0; foreach my $fh ( @ready ) { my $executer = $self->{'executers'}->{$fh}; unless ($executer) { $self->error_message("Cannot determine RPC executer for file handle $fh fileno ",$fh->fileno); return; } $count++; unless ($executer->execute($self) ) { # they told us they were done $self->select->remove($fh); delete $self->{'executers'}->{$fh}; } } return $count; } 1; =pod =head1 NAME UR::Service::RPC::Server - Class for implementing RPC servers =head1 SYNOPSIS my $executer = Some::Exec::Class->create(fh => $fh); my $server = UR::Service::RPC::Server->create(); $server->add_executer($executer); $server->loop(5); # Process messages for 5 seconds =head1 DESCRIPTION The RPC server implementation isn't fleshed out very well yet, and may change in the future. =head1 METHODS =over 4 =item add_executer $server->add_executer($exec); Incorporate a new UR::Service::RPC::Executer instance to this server. It adds the Executer's filehandle to its own internal IO::Select object. =item loop $server->loop(); $server->loop(0); $server->loop($timeout); Enter the Server's event loop for the given number of seconds. If the timeout is undef, it will stay in the loop forever. If the timeout is 0, it will make a single pass though the readable filehandles and call C on their Executer objects. If the return value of an Executer's C method is false, that Executer's file handle is removed from the internal Select object. =back =head1 SEE ALSO UR::Service::RPC::Executer, UR::Service::RPC::Message =cut TcpConnectionListener.pm100664023532023421 163412544604516 21573 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/RPCpackage UR::Service::RPC::TcpConnectionListener; use UR; use strict; use warnings; our $VERSION = "0.44"; # UR $VERSION; class UR::Service::RPC::TcpConnectionListener { is => 'UR::Service::RPC::Executer', }; sub execute { my($self,$rpcserver) = @_; my $fh = $self->fh; my $socket = $fh->accept(); unless ($self->authenticate($socket)) { $socket->close(); return; } my $exec = $self->create_worker($socket); $rpcserver->add_executer($exec); return $exec; } # Sub classes can override this sub authenticate { # my($self,$new_socket) = @_; return 1; } # Child classes can override either of these to get custom behavior sub worker_class_name { 'UR::Service::RPC::Executer'; } sub create_worker { my($self,$new_socket) = @_; my $class = $self->worker_class_name; my $exec = $class->create(fh => $new_socket); return $exec; } 1; UrlRouter.pm100664023532023421 753712544604516 16646 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Servicepackage UR::Service::UrlRouter; use strict; use warnings; use UR; use Sub::Install; use overload '&{}' => \&__call__, # To support being called as a code ref 'bool' => sub { 1 }; # Required due to an unless() test in UR::Context class UR::Service::UrlRouter { has_optional => [ verbose => { is => 'Boolean' }, ] }; foreach my $method ( qw( GET POST PUT DELETE ) ) { my $code = sub { my($self, $path, $sub) = @_; my $list = $self->{$method} ||= []; push @$list, [ $path, $sub ]; }; Sub::Install::install_sub({ as => $method, code => $code, }); } sub _log { my $self = shift; return unless $self->verbose; print STDERR join("\t", @_),"\n"; } sub __call__ { my $self = shift; return sub { my $env = shift; my $req_method = $env->{REQUEST_METHOD}; my $matchlist = $self->{$req_method} || []; foreach my $route ( @$matchlist ) { my($path,$cb) = @$route; my $call = sub { my $rv = $cb->($env, @_); $self->_log(200, $req_method, $env->{PATH_INFO}, $path); return ref($rv) ? $rv : [ 200, [], [$rv] ]; }; if (my $ref = ref($path)) { if ($ref eq 'Regexp' and (my @matches = $env->{PATH_INFO} =~ $path)) { return $call->(@matches); } elsif ($ref eq 'CODE' and $path->($env)) { return $call->(); } } elsif ($env->{PATH_INFO} eq $path) { return $call->(); } } $self->_log(404, $req_method, $env->{PATH_INFO}); return [ 404, [ 'Content-Type' => 'text/plain' ], [ 'Not Found' ] ]; } } 1; =pod =head1 NAME UR::Service::UrlRouter - PSGI-aware router for incoming requests =head1 SYNOPSIS my $r = UR::Service::UrlRouter->create(); $r->GET('/index.html', \&handle_index); $r->POST(qr(update/(.*?).html, \&handle_update); my $s = UR::Service::WebServer->create(); $s->run( $r ); =head1 DESCRIPTION This class acts as a middleman, routing requests from a PSGI server to the appropriate function to handle the requests. =head2 Properties =over 4 =item verbose If verbose is true, the object will print details about the handled requests to STDOUT. =back =head2 Methods =over 4 =item $r->GET($URLish, $handler) =item $r->POST($URLish, $handler) =item $r->PUT($URLish, $handler) =item $r->DELETE($URLisn, $handler) These four methods register a handler for the given request method + URL pair. The first argument specifies the URL to match against, It can be specified in one of the following ways =over 4 =item $string A simple string matches the incoming request if the request's path is eq to the $string =item qr(some regex (with) captures) A regex matches the incoming request if the path matches the regex. If the regex contains captures, these are passed as additional arguments to the $handler. =item $coderef A coderef matches the incoming request if $coderef returns true. $coderef is given one acgument: the PSGI env hashref. $handler is a CODE ref. When called, the first argument is the standard PSGI env hashref. =back =item $r->__call__ __call__ is not intended to be called directly. This class overloads the function dereference (call) operator so that the object may be used as a callable object (ie. $obj->(arg, arg)). As overload expects, __call__ returns a code ref that handles the PSGI request by finding an appropriate match with the incoming request and a previously registered handler. If no matching handler is found, it returns a 404 error code. If multiple handlers match the incoming request, then only the earliest registered handler will be called. =back =head1 SEE ALSO L, L, L =cut WebServer.pm100664023532023421 1457512544604516 16627 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Servicepackage UR::Service::WebServer; use strict; use warnings; use UR; use UR::Service::WebServer::Server; use IO::File; use IO::Socket::INET; use Sys::Hostname; class UR::Service::WebServer { has => [ host => { is => 'String', default_value => 'localhost', doc => 'IP address to listen on' }, port => { is => 'Integer', default_value => undef, doc => 'TCP port to listen on' }, ], has_optional => [ server => { is => 'HTTP::Server::PSGI', calculate_from => ['__host','__port'], is_constant => 1, calculate => q( return UR::Service::WebServer::Server->new( host => $__host, port => $__port, timeout => $self->timeout, server_ready => sub { $self->announce() }, ); ), }, timeout => { is => 'Integer', default_value => undef, doc => 'Timeout for read and write events' }, idle_timeout => { is => 'Integer', default_value => undef, doc => 'Exit the event loop after being idle for this many seconds' }, cb => { is => 'CODE', doc => 'callback for handling requests' }, ], }; # Override port and host so they can auto-fill when needed sub _port_host_override { my $self = shift; my $methodname = shift; my $method = '__'.$methodname; my $socket_method = 'sock'.$methodname; if (@_) { if ($self->{server}) { die "Cannot change $methodname after it has created the listen socket"; } $self->$method(@_); } else { # if (!defined($self->$method) && !defined($self->{server})) { unless (defined $self->$method) { unless (defined $self->{server}) { # not connected yet - start the server's listen socket and get its port $self->server->setup_listener(); } $self->$method( $self->server->listen_sock->$socket_method() ); } } return $self->$method; } sub port { my $self = shift; $self->_port_host_override('port', @_); } sub host { my $self = shift; $self->_port_host_override('host', @_); } sub announce { my $self = shift; my $sock = $self->server->listen_sock; my $host = ($sock->sockhost eq '0.0.0.0') ? Sys::Hostname::hostname() : gethostbyaddr($sock->sockaddr, AF_INET); $self->status_message(sprintf('Listening on http://%s:%d/', $host, $sock->sockport)); return 1; } sub run { my $self = shift; my $cb = shift || $self->cb; unless ($cb) { $self->warning_message("No callback for run()... returning"); return; } my $timeout = $self->idle_timeout || 0; local $SIG{'ALRM'} = sub { die "alarm\n" }; eval { alarm($timeout); $self->server->run($cb); }; alarm(0); die $@ unless $@ eq "alarm\n"; } my %mime_types = ( 'js' => 'application/javascript', 'html' => 'text/html', 'css' => 'text/css', '*' => 'text/plain', ); sub _mime_type_for_filename { my($self, $pathname) = @_; my($ext) = ($pathname =~ m/\.(\w+)$/); $ext ||= '*'; return $mime_types{$ext} || $mime_types{'*'}; } sub _file_opener_for_directory { my($self, $dir) = @_; return sub { (my $pathname = shift) =~ s#/?\.\.##g; # Remove .. - don't want them escaping the given directory tree return IO::File->new( join('/', $dir, $pathname), 'r'); }; } sub file_handler_for_directory { my($self, $dir) = @_; my $opener = $self->_file_opener_for_directory($dir); return sub { my($env, $pathname) = @_; my $fh = $opener->($pathname); unless($fh) { return [ 404, [ 'Content-Type' => 'text/plain'], ['Not Found']]; } my $type = $self->_mime_type_for_filename($pathname); if ($env->{'psgi.streaming'}) { return [ 200, ['Content-Type' => $type], $fh]; } else { local $/; my $buffer = <$fh>; return [ 200, ['Content-Type' => $type], [$buffer]]; } }; } sub delete { my $self = shift; $self->server->listen_sock->close(); $self->{server} = undef; $self->SUPER::delete(@_); } 1; =pod =head1 NAME UR::Service::WebServer - A PSGI-based web server =head1 SYNOPSIS my $s = UR::Service::WebServer(port => 4321); $s->run( \&handle_request ); =head1 DESCRIPTION Implements a simple, standalone web server based on HTTP::Server::PSGI. The event loop is entered by calling the run() method. =head2 Properties =over 4 =item host The IP address to listen on for connections. The default value is 'localhost'. host can be changed any time before the server is created, usually the first time run() is called. =item port The TCP port to listen on for connections. The detault value is undef, meaning that the system will pick an unused port. port can be changed any time before the server is created, usually the first time run() is called. =item server Holds a reference to an object that isa HTTP::Server::PSGI. This will be automaticly created the first time run() is called. =item cb Holds a CODE reference used as the default request handler within run(). =back =head2 Methods =over 4 =item $self->announce() This method is called when the PSGI server is ready to accept requests. The base-class behavior is to print the listening URL on STDOUT. Subclasses can override it to implement their own behavior. =item my $code = $self->file_handler_for_directory($path) A helper method used for implementing server for files located in the directory $path. It returns a CODE ref that takes 2 arguments, $env (the standard PSGI env hashref) and $pathname (a path relative to $path). It returns the standard tuple a PSGI server expects. $pathname is pre-processed by removing all occurances of ".." to keep requests within the provided $path. If the requested file is not found, then it returns a 404. =item $self->run(<$cb>) Enter the request loop. If a callback is not provided to run(), then the object's cb property is used instead. If neither have a value, then run() returns immediately. For each request $cb is called with one argument, the standard PSGI env hashref. =back =head1 SEE ALSO L =cut Server.pm100664023532023421 207612544604516 20046 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/WebServerpackage UR::Service::WebServer::Server; use strict; use warnings; use base 'HTTP::Server::PSGI'; # Override new because the default constructor doesn't accept a 'port' argument of # undef to make the system pick a port sub new { my($class, %args) = @_; my %supplied_port_arg; if (exists $args{port}) { $supplied_port_arg{port} = delete $args{port}; } my $self = $class->SUPER::new(%args); if (%supplied_port_arg) { $self->{port} = $supplied_port_arg{port}; } return $self; } sub listen_sock { return shift->{listen_sock}; } # pre-fill read data for the test sub buffer_input { my $self = shift; $self->{__buffer_input__} = shift; } sub read_timeout { my $self = shift; my($sock, $buf, $len, $off, $timeout) = @_; if ($self->{__buffer_input__}) { $$buf = ref $self->{__buffer_input__} ? $self->{__buffer_input__}->() : $self->{__buffer_input__}; delete $self->{__buffer_input__}; return length($$buf); } $self->SUPER::read_timeout(@_); } 1; json.js100664023532023421 2572112544604516 15667 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service/* json.js 2008-01-17 Public Domain No warranty expressed or implied. Use at your own risk. This file has been superceded by http://www.JSON.org/json2.js See http://www.JSON.org/js.html This file adds these methods to JavaScript: array.toJSONString(whitelist) boolean.toJSONString() date.toJSONString() number.toJSONString() object.toJSONString(whitelist) string.toJSONString() These methods produce a JSON text from a JavaScript value. It must not contain any cyclical references. Illegal values will be excluded. The default conversion for dates is to an ISO string. You can add a toJSONString method to any date object to get a different representation. The object and array methods can take an optional whitelist argument. A whitelist is an array of strings. If it is provided, keys in objects not found in the whitelist are excluded. string.parseJSON(filter) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional filter parameter is a function which can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. If a key contains the string 'date' then // convert the value to a date. myData = text.parseJSON(function (key, value) { return key.indexOf('date') >= 0 ? new Date(value) : value; }); It is expected that these methods will formally become part of the JavaScript Programming Language in the Fourth Edition of the ECMAScript standard in 2008. This file will break programs with improper for..in loops. See http://yuiblog.com/blog/2006/09/26/for-in-intrigue/ This is a reference implementation. You are free to copy, modify, or redistribute. Use your own copy. It is extremely unwise to load untrusted third party code into your pages. */ /*jslint evil: true */ /*members "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply, charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, parseJSON, prototype, push, replace, test, toJSONString, toString */ // Augment the basic prototypes if they have not already been augmented. if (!Object.prototype.toJSONString) { Array.prototype.toJSONString = function (w) { var a = [], // The array holding the partial texts. i, // Loop counter. l = this.length, v; // The value to be stringified. // For each value in this array... for (i = 0; i < l; i += 1) { v = this[i]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Treat objects thats lack the // toJSONString method as null. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v && typeof v.toJSONString === 'function') { a.push(v.toJSONString(w)); } else { a.push('null'); } break; case 'string': case 'number': case 'boolean': a.push(v.toJSONString()); break; default: a.push('null'); } } // Join all of the member texts together and wrap them in brackets. return '[' + a.join(',') + ']'; }; Boolean.prototype.toJSONString = function () { return String(this); }; Date.prototype.toJSONString = function () { // Eventually, this method will be based on the date.toISOString method. function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return '"' + this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z"'; }; Number.prototype.toJSONString = function () { // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(this) ? String(this) : 'null'; }; Object.prototype.toJSONString = function (w) { var a = [], // The array holding the partial texts. k, // The current key. i, // The loop counter. v; // The current value. // If a whitelist (array of keys) is provided, use it assemble the components // of the object. if (w) { for (i = 0; i < w.length; i += 1) { k = w[i]; if (typeof k === 'string') { v = this[k]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Ignore objects that lack the // toJSONString method. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v) { if (typeof v.toJSONString === 'function') { a.push(k.toJSONString() + ':' + v.toJSONString(w)); } } else { a.push(k.toJSONString() + ':null'); } break; case 'string': case 'number': case 'boolean': a.push(k.toJSONString() + ':' + v.toJSONString()); // Values without a JSON representation are ignored. } } } } else { // Iterate through all of the keys in the object, ignoring the proto chain // and keys that are not strings. for (k in this) { if (typeof k === 'string' && Object.prototype.hasOwnProperty.apply(this, [k])) { v = this[k]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Ignore objects that lack the // toJSONString method. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v) { if (typeof v.toJSONString === 'function') { a.push(k.toJSONString() + ':' + v.toJSONString()); } } else { a.push(k.toJSONString() + ':null'); } break; case 'string': case 'number': case 'boolean': a.push(k.toJSONString() + ':' + v.toJSONString()); // Values without a JSON representation are ignored. } } } } // Join all of the member texts together and wrap them in braces. return '{' + a.join(',') + '}'; }; (function (s) { // Augment String.prototype. We do this in an immediate anonymous function to // avoid defining global variables. // m is a table of character substitutions. var m = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; s.parseJSON = function (filter) { var j; function walk(k, v) { var i, n; if (v && typeof v === 'object') { for (i in v) { if (Object.prototype.hasOwnProperty.apply(v, [i])) { n = walk(i, v[i]); if (n !== undefined) { v[i] = n; } } } } return filter(k, v); } // Parsing happens in three stages. In the first stage, we run the text against // a regular expression which looks for non-JSON characters. We are especially // concerned with '()' and 'new' because they can cause invocation, and '=' // because it can cause mutation. But just to be safe, we will reject all // unexpected characters. // We split the first stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace all backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/.test(this.replace(/\\./g, '@'). replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the second stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + this + ')'); // In the optional third stage, we recursively walk the new structure, passing // each name/value pair to a filter function for possible transformation. return typeof filter === 'function' ? walk('', j) : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('parseJSON'); }; s.toJSONString = function () { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can simply slap some quotes around it. // Otherwise we must also replace the offending characters with safe // sequences. if (/["\\\x00-\x1f]/.test(this)) { return '"' + this.replace(/[\x00-\x1f\\"]/g, function (a) { var c = m[a]; if (c) { return c; } c = a.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }) + '"'; } return '"' + this + '"'; }; })(String.prototype); }urinterface.js100664023532023421 1036112544604516 17217 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Service// Here's the basic API: // var UR = new URInterface('http://server/api_root'); // var gsc_pse = UR.get_class('GSC::PSE'); // var pse_obj = gsc_pse.get(10001); function construct_xmlhttp() { var xmlhttp = null; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); if ( typeof xmlhttp.overrideMimeType != 'undefined') { xmlhttp.overrideMimeType('text/xml'); } } else if (window.ActiveXObject) { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } else { alert('Perhaps your browser does not support xmlhttprequests?'); } return xmlhttp; } // a URInterface holds the info to connect to the server function URInterface(base_url) { this.base_url = base_url; this.get_class = function(class_name) { return new URClassInterface(this.base_url, class_name); } this.commit = function() { var url = this.base_url + '/class/UR/Context'; do_rpc(url, 'commit', []); } } function do_rpc(url, method,arglist) { // There must be some strange scoping rules going on here. // To get the struct encoded properly, I need to copy the passed-in // array to a local one. var params = new Array; for (var i = 0; i < arglist.length; i++) { params.push(arglist[i]); } var json_rpc = { "method":method,"params":params }; //var json_rpc = { "method":method, "params":arglist }; xmlhttp = construct_xmlhttp(); xmlhttp.open('POST', url, false); post_data = json_rpc.toJSONString(); xmlhttp.send(post_data); var resultstring = xmlhttp.responseText; var resultobj = resultstring.parseJSON(); if (resultobj.error) { alert(resultobj.error); return null; } return resultobj.result; } // a URClassInterface holds the info necessary for getting instances of a class from the server function URClassInterface(base_url,class_name) { this.class_name = class_name; var path_parts = new Array; path_parts = class_name.split('::'); this.url = base_url + '/class/' + path_parts.join('/'); var result = do_rpc(this.url, '_get_class_info', []); this.id_properties = result[0]["id_properties"]; this.properties = result[0]["properties"]; this.methods = result[0]["methods"]; this.get = function() { var returned_list = do_rpc(this.url, 'get', arguments); var retval = new Array; for(var i = 0; i < returned_list.length; i++) { delete returned_list[i].db_committed; delete returned_list[i].toJSONString; var obj_url = base_url + '/obj/' + path_parts.join('/') + '/' + returned_list[i].id; var theobj = new URObject(returned_list[i], obj_url); theobj.add_methods(this.properties); theobj.add_methods(this.methods); retval.push(theobj); } return retval; }; } // Yer basic object instance from the server. For now it holds all the attributes // of an object. But we'll move it to only holding ID properties soonly function URObject(thing,url) { for (var i in thing) { this[i] = thing[i]; } this.url = url; this.tableize = function(display_location) { var table = ''; for (var i in this) { if (typeof(this[i]) == 'function') { continue; } table += ''; } table += '
    ' + this.object_type + '
    KeyValue
    ' + i + '' + this[i] + '
    '; var orig_data = document.getElementById(display_location).innerHTML; document.getElementById(display_location).innerHTML = orig_data + table; }; this.add_methods = function(method_names) { for (var i = 0; i < method_names.length; i++) { var method_name = method_names[i]; this[method_name] = function() { do_rpc(this.url, method_name, arguments); } } }; this.call = function(method,arglist) { //var arglist = new Array; //for (var i = 1; i < arguments.length; i++) { // arglist.push(arguments[i]); //} var returned_list = do_rpc(this.url, method, arglist); return returned_list; }; } Singleton.pm100664023532023421 1460412544604516 15256 0ustar00abrummetgsc000000000000UR-0.44/lib/UR package UR::Singleton; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Singleton', is => ['UR::Object'], is_abstract => 1, ); sub id { my $self = shift; return (ref $self ? $self->SUPER::id(@_) : $self); } sub _init_subclass { my $class_name = shift; my $class_meta_object = $class_name->__meta__; # Write into the class's namespace the correct singleton overrides # to standard UR::Object methods. my $src; if ($class_meta_object->is_abstract) { $src = qq|sub ${class_name}::_singleton_object { Carp::confess("${class_name} is an abstract singleton! Select a concrete sub-class.") }| . "\n" . qq|sub ${class_name}::_singleton_class_name { Carp::confess("${class_name} is an abstract singleton! Select a concrete sub-class.") }| . "\n" . qq|sub ${class_name}::_load { shift->_abstract_load(\@_) }| } else { $src = qq|sub ${class_name}::_singleton_object { \$${class_name}::singleton or shift->_concrete_load() }| . "\n" . qq|sub ${class_name}::_singleton_class_name { '${class_name}' }| . "\n" . qq|sub ${class_name}::_load { shift->_concrete_load(\@_) }| . "\n" . qq|sub ${class_name}::get { shift->_concrete_get(\@_) }| . "\n" . qq|sub ${class_name}::is_loaded { shift->_concrete_is_loaded(\@_) }| ; } eval $src; Carp::confess($@) if $@; return 1; } # Abstract singletons havd a different load() method than concrete ones. # We could do this with forking logic, but since many of the concrete methods # get non-default handling, it's more efficient to do it this way. sub _abstract_load { my $class = shift; my $bx = $class->define_boolexpr(@_); my $id = $bx->value_for_id; unless (defined $id) { use Data::Dumper; my $params = { $bx->params_list }; Carp::confess("Cannot load a singleton ($class) except by specific identity. " . Dumper($params)); } my $subclass_name = $class->_resolve_subclass_name_for_id($id); eval "use $subclass_name"; if ($@) { undef $@; return; } return $subclass_name->get(); } # Concrete singletons have overrides to the most basic acccessors to # accomplish class/object duality smoothly. sub _concrete_get { if (@_ == 1 or (@_ == 2 and $_[0] eq $_[1])) { my $self = $_[0]->_singleton_object; return $self if $self; } return shift->_concrete_load(@_); } sub _concrete_is_loaded { if (@_ == 1 or (@_ == 2 and $_[0] eq $_[1])) { my $self = $_[0]->_singleton_object; return $self if $self; } return shift->SUPER::is_loaded(@_); } sub _concrete_load { my $class = shift; $class = ref($class) || $class; no strict 'refs'; my $varref = \${ $class . "::singleton" }; unless ($$varref) { my $id = $class->_resolve_id_for_subclass_name($class); my $class_object = $class->__meta__; my @prop_names = $class_object->all_property_names; my %default_values; foreach my $prop_name ( @prop_names ) { my $prop = $class_object->property_meta_for_name($prop_name); next unless $prop; my $val = $prop->{'default_value'}; next unless defined $val; $default_values{$prop_name} = $val; } $$varref = $UR::Context::current->_construct_object($class,%default_values, id => $id); $$varref->{db_committed} = { %$$varref }; $$varref->__signal_change__("load"); Scalar::Util::weaken($$varref); } my $self = $class->_concrete_is_loaded(@_); return unless $self; unless ($self->init) { Carp::confess("Failed to initialize singleton $class!"); } return $self; } # This is implemented in the singleton to do any post-load processing. sub init { return 1; } # All singletons require special deletion logic since they keep a #weakened reference to the singleton. sub delete { my $self = shift; my $class = $self->class; $self->SUPER::delete(); no strict 'refs'; ${ $class . "::singleton" } = undef if ${ $class . "::singleton" } eq $self; return $self; } # In most cases, the id is the class name itself, but this is not necessary. sub _resolve_subclass_name_for_id { my $class = shift; my $id = shift; return $id; } sub _resolve_id_for_subclass_name { my $class = shift; my $subclass_name = shift; return $subclass_name; } sub create { my $class = shift; my $bx = $class->define_boolexpr(@_); my $id = $bx->value_for_id; unless (defined $id) { Carp::confess("No singleton ID class specified for constructor?"); } my $subclass = $class->_resolve_subclass_name_for_id($id); eval "use $subclass"; unless ($subclass->isa(__PACKAGE__)) { eval '@' . $subclass . "::ISA = ('" . __PACKAGE__ . "')"; } return $subclass->_concrete_get(); } 1; =pod =head1 NAME UR::Singleton - Abstract class for implementing singleton objects =head1 SYNOPSIS package MyApp::SomeClass; use UR; class MyApp::SomeClass { is => 'UR::Singleton', has => [ foo => { is => 'Number' }, ] }; $obj = MyApp::SomeClass->get(); $obj->foo(1); =head1 DESCRIPTION This class provides the infrastructure for singleton classes. Singletons are classes of which there can only be one instance, and that instance's ID is the class name. If a class inherits from UR::Singleton, it overrides the default implementation of C and C in UR::Object with code that fabricates an appropriate object the first time it's needed. Singletons are most often used as one of the parent classes for data sources within a Namespace. This makes it convienent to refer to them using only their name, as in a class definition. =head1 METHODS =over 4 =item _singleton_object $obj = Class::Name->_singleton_object; $obj = $obj->_singleton_object; Returns the object instance whether it is called as a class or object method. =item _singleton_class_name $class_name = Class::Name->_singleton_class_name; $class_name = $obj->_singleton_class_name; Returns the class name whether it is called as a class or object method. =back =head1 SEE ALSO UR::Object =cut Test.pm100664023532023421 55212544604516 14170 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Test; use base 'Test::Builder::Module'; use strict; use warnings; use Exporter 'import'; our @EXPORT_OK = qw(txtest); sub txtest { my ($name, $subtests, @args) = @_; my $tb = __PACKAGE__->builder; my $tx = UR::Context::Transaction->begin(); my $rv = $tb->subtest($name, $subtests, @args); $tx->rollback; return $rv; }; 1; Util.pm100664023532023421 4453212544604516 14234 0ustar00abrummetgsc000000000000UR-0.44/lib/UR package UR::Util; use warnings; use strict; require UR; our $VERSION = "0.44"; # UR $VERSION; use Cwd; use Data::Dumper; use Clone::PP; use Config; use Module::Runtime v0.014 qw(module_notional_filename); sub on_destroy(&) { my $sub = shift; unless ($sub) { Carp::confess("expected an anonymous sub!") } return bless($sub, "UR::Util::CallOnDestroy"); } # used only by the above sub # the local $@ ensures that we this does not stomp on thrown exceptions sub UR::Util::CallOnDestroy::DESTROY { local $@; shift->(); } sub d { Data::Dumper->new([@_])->Terse(1)->Indent(0)->Useqq(1)->Dump; } sub null_sub { } sub used_libs { my @extra; my @compiled_inc = UR::Util::compiled_inc(); my @perl5lib = split(':', $ENV{PERL5LIB}); map { $_ =~ s/\/+$// } (@compiled_inc, @perl5lib); # remove trailing slashes map { $_ = Cwd::abs_path($_) || $_ } (@compiled_inc, @perl5lib); for my $inc (@INC) { $inc =~ s/\/+$//; my $abs_inc = Cwd::abs_path($inc) || $inc; # should already be expanded by UR.pm next if (grep { $_ =~ /^$abs_inc$/ } @compiled_inc); next if (grep { $_ =~ /^$abs_inc$/ } @perl5lib); next if ((File::Spec->splitdir($inc))[-1] eq $Config{archname}); push @extra, $inc; } unshift @extra, ($ENV{PERL_USED_ABOVE} ? split(":", $ENV{PERL_USED_ABOVE}) : ()); map { $_ =~ s/\/+$// } @extra; # remove trailing slashes again @extra = _unique_elements(@extra); return @extra; } sub _unique_elements { my @list = @_; my %seen = (); my @unique = grep { ! $seen{$_} ++ } @list; return @unique; } sub used_libs_perl5lib_prefix { my $prefix = ""; for my $i (used_libs()) { $prefix .= "$i:"; } return $prefix; } sub touch_file { my $filename = shift; open(my $fh, '>>', $filename); } my @compiled_inc; BEGIN { use Config; my @var_list = ( 'updatesarch', 'updateslib', 'archlib', 'privlib', 'sitearch', 'sitelib', 'sitelib_stem', 'vendorarch', 'vendorlib', 'vendorlib_stem', 'extrasarch', 'extraslib', ); for my $var_name (@var_list) { if ($var_name =~ /_stem$/ && $Config{$var_name}) { my @stem_list = (split(' ', $Config{'inc_version_list'}), ''); push @compiled_inc, map { $Config{$var_name} . "/$_" } @stem_list } else { push @compiled_inc, $Config{$var_name} if $Config{$var_name}; } } # UR locks in relative paths when loaded so instead of adding '.' we add cwd push @compiled_inc, Cwd::cwd() if (${^TAINT} == 0); map { $_ =~ s/\/+/\//g } @compiled_inc; map { $_ =~ s/\/+$// } @compiled_inc; } sub compiled_inc { return @compiled_inc; } sub deep_copy { return Clone::PP::clone($_[0]); } sub value_positions_map { my ($array) = @_; my %value_pos; for (my $pos = 0; $pos < @$array; $pos++) { my $value = $array->[$pos]; if (exists $value_pos{$value}) { die "Array has duplicate values, which cannot unambiguously be given value positions!" . Data::Dumper::Dumper($array); } $value_pos{$value} = $pos; } return \%value_pos; } sub positions_of_values { # my @pos = positions_of_values(\@unordered_crap, \@correct_order); # my @fixed = @unordered_crap[@pos]; my ($unordered_array,$ordered_array) = @_; my $map = value_positions_map($unordered_array); my @translated_positions; $#translated_positions = $#$ordered_array; for (my $pos = 0; $pos < @$ordered_array; $pos++) { my $value = $ordered_array->[$pos]; my $unordered_position = $map->{$value}; $translated_positions[$pos] = $unordered_position; } # self-test: # my @now_ordered = @$unordered_array[@translated_positions]; # unless ("@now_ordered" eq "@$ordered_array") { # Carp::confess() # } return @translated_positions; } # Get all combinations of values # input is a list of listrefs of values sub combinations_of_values { return [] unless @_; my $first_values = shift; $first_values = [ $first_values ] unless (ref($first_values) and ref($first_values) eq 'ARRAY'); my @retval; foreach my $sub_combination ( &combinations_of_values(@_) ) { foreach my $value ( @$first_values ) { push @retval, [$value, @$sub_combination]; } } return @retval; } # generate a method sub _define_method { my $class = shift; my (%opts) = @_; # create method name my $method = $opts{pkg} . '::' . $opts{property}; # determine return value type my $retval; if (defined($opts{value})) { my $refval = ref($opts{value}); $retval = ($refval) ? $refval : 'SCALAR'; } else { $retval = 'SCALAR'; } # start defining method my $substr = "sub $method { my \$self = shift; "; # set default value $substr .= "\$self->{$opts{property}} = "; my $dd = Data::Dumper->new([ $opts{value} ]); $dd->Terse(1); # do not print ``$VAR1 ='' $substr .= $dd->Dump; $substr .= " unless defined(\$self->{$opts{property}}); "; # array or scalar? if ($retval eq 'ARRAY') { if ($opts{access} eq 'rw') { # allow setting of array $substr .= "\$self->{$opts{property}} = [ \@_ ] if (\@_); "; } # add return value $substr .= "return \@{ \$self->{$opts{property}} }; "; } else { # scalar if ($opts{access} eq 'rw') { # allow setting of scalar $substr .= "\$self->{$opts{property}} = \$_[0] if (\@_); "; } # add return value $substr .= "return \$self->{$opts{property}}; "; } # end the subroutine definition $substr .= "}"; # actually define the method no warnings qw(redefine); eval($substr); if ($@) { # fatal error since this is like a failed compilation die("failed to defined method $method {$substr}:$@"); } return 1; } =pod =over =item path_relative_to $rel_path = UR::Util::path_relative_to($base, $target); Returns the pathname to $target relative to $base. If $base and $target are the same, then it returns '.'. If $target is a subdirectory of of $base, then it returns the portion of $target that is unique compared to $base. If $target is not a subdirectory of $base, then it returns a relative pathname starting with $base. =back =cut sub path_relative_to { my($base,$target) = @_; $base = Cwd::abs_path($base); $target = Cwd::abs_path($target); my @base_path_parts = split('/', $base); my @target_path_parts = split('/', $target); my $i; for ($i = 0; $i < @base_path_parts and $base_path_parts[$i] eq $target_path_parts[$i]; $i++ ) { ; } my $rel_path = '../' x (scalar(@base_path_parts) - $i) . join('/', @target_path_parts[$i .. $#target_path_parts]); $rel_path = '.' unless length($rel_path); return $rel_path; } =pod =over =item generate_readwrite_methods UR::Util->generate_readwrite_methods ( some_scalar_property => 1, some_array_property => [] ); This method generates accessor/set methods named after the keys of its hash argument. The type of function generated depends on the default value provided as the hash key value. If the hash key is a scalar, a scalar method is generated. If the hash key is a reference to an array, an array method is generated. This method does not overwrite class methods that already exist. =back =cut sub generate_readwrite_methods { my $class = shift; my %properties = @_; # get package of caller my $pkg = caller; # loop through properties foreach my $property (keys(%properties)) { # do not overwrite defined methods next if $pkg->can($property); # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'rw' ); } return 1; } =pod =over =item generate_readwrite_methods_override UR::Util->generate_readwrite_methods_override ( some_scalar_property => 1, some_array_property => [] ); Same as generate_readwrite_function except that we force the functions into the namespace even if the function is already defined =back =cut sub generate_readwrite_methods_override { my $class = shift; my %properties = @_; # get package of caller my $pkg = caller; # generate the methods for each property foreach my $property (keys(%properties)) { # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'rw' ); } return 1; } =pod =over =item generate_readonly_methods UR::Util->generate_readonly_methods ( some_scalar_property => 1, some_array_property => [] ); This method generates accessor methods named after the keys of its hash argument. The type of function generated depends on the default value provided as the hash key value. If the hash key is a scalar, a scalar method is generated. If the hash key is a reference to an array, an array method is generated. This method does not overwrite class methods that already exist. =back =cut sub generate_readonly_methods { my $class = shift; my %properties = @_; # get package of caller my ($pkg) = caller; # loop through properties foreach my $property (keys(%properties)) { # do no overwrite already defined methods next if $pkg->can($property); # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'ro' ); } return 1; } =pod =over =item object my $o = UR::Util::object($something); Return the object form of the supplied argument. For regular objects, it returns the argument unchanged. For singleton class names, it returns the instance of the Singleton. For other class names, it throws an exception. =back =cut sub object { my $it = shift; unless (ref $it) { if ($it->isa('UR::Singleton')) { $it = $it->_singleton_object(); } else { Carp::croak("Expected an object instance or Singleton class name, but got '$it'"); } } return $it; } =pod =over =item mapreduce_grep my @matches = UR::Util->map_reduce_grep { shift->some_test } @candidates; Works similar to the Perl C builtin, but in a possibly-parallel fashion. If the environment variable UR_NR_CPU is set to a number greater than one, it will fork off child processes to perform the test on slices of the input list, collect the results, and return the matching items as a list. The test function is called with a single argument, an item from the list to be tested, and should return a true of false value. =back =cut sub mapreduce_grep($&@) { my $class = shift; my $subref = shift; #$DB::single = 1; # First check fast... should we do parallel at all? if (!$ENV{'UR_NR_CPU'} or $ENV{'UR_NR_CPU'} < 2) { #return grep { $subref->($_) } @_; my @ret = grep { $subref->($_) } @_; return @ret; } my(@read_handles, @child_pids); my $cleanup = sub { foreach my $handle ( @read_handles ) { $handle->close(); } kill 'TERM', @child_pids; foreach my $pid ( @child_pids ) { waitpid($pid,0); } }; my @things_to_check = @_; my($children, $length,$parent_last); if ($ENV{'UR_NR_CPU'}) { $length = POSIX::ceil(scalar(@things_to_check) / $ENV{'UR_NR_CPU'}); $children = $ENV{'UR_NR_CPU'} - 1; } else { $children = 0; $parent_last = $#things_to_check; } # FIXME - There needs to be some code in here to disconnect datasources # Oracle in particular (maybe all DBs?), stops working right unless you # disconnect before forking my $start = $length; # First child starts checking after parent's range $parent_last = $length - 1; while ($children-- > 0) { my $pipe = IO::Pipe->new(); unless ($pipe) { Carp::carp("pipe() failed: $!\nUnable to create pipes to communicate with child processes to verify transact+ion, falling back to serial verification"); $cleanup->(); $parent_last = $#things_to_check; last; } my $pid = fork(); if ($pid) { $pipe->reader(); push @read_handles, $pipe; $start += $length; } elsif (defined $pid) { $pipe->writer(); my $last = $start + $length; $last = $#things_to_check if ($last > $#things_to_check); #my @objects = grep { $subref->($_) } @things_to_check[$start .. $last]; my @matching; for (my $i = $start; $i <= $last; $i++) { if ($subref->($things_to_check[$i])) { push @matching, $i; } } # FIXME - when there's a more general framework for passing objects between # processes, use that instead #$pipe->printf("%s\n%s\n",$_->class, $_->id) foreach @objects; $pipe->print("$_\n") foreach @matching; exit; } else { Carp::carp("fork() failed: $!\nUnable to create child processes to ver+ify transaction, falling back to seri+al verification"); $cleanup->(); $parent_last = $#things_to_check; } } my @matches = grep { $subref->($_) } @things_to_check[0 .. $parent_last]; foreach my $handle ( @read_handles ) { READ_FROM_CHILD: while(1) { my $match_idx = $handle->getline(); last READ_FROM_CHILD unless $match_idx; chomp $match_idx; push @matches, $things_to_check[$match_idx]; #my $match_class = $handle->getline(); #last READ_FROM_CHILD unless $match_class; #chomp($match_class); #my $match_id = $handle->getline(); #unless (defined $match_id) { # Carp::carp("Protocol error. Tried to get object ID for class $match_class while verifying transaction"+); # last READ_FROM_CHILD; #} #chomp($match_id); #push @objects, $match_class->get($match_id); } $handle->close(); } $cleanup->(); return @matches; } # Used in several places when printing out hash-like parameters # to the user, such as in error messages sub display_string_for_params_list { my $class = shift; my %params; if (ref($_[0]) =~ 'HASH') { %params = %{$_[0]}; } else { %params = @_; } my @strings; foreach my $key ( keys %params ) { my $val = $params{$key}; $val = defined($val) ? "'$val'" : '(undef)'; push @strings, "$key => $val"; } return join(', ', @strings); } # why isn't something like this in List::Util? # Return a list of 3 listrefs: # 0: items common to both lists # 1: items in the first list only # 2: items in the second list only sub intersect_lists { my ($m,$n) = @_; my %shared; my %monly; my %nonly; @monly{@$m} = @$m; for my $v (@$n) { if ($monly{$v}) { $shared{$v} = delete $monly{$v}; } else{ $nonly{$v} = $v; } } return ( [ values %shared ], [ values %monly ], [ values %nonly ], ); } sub is_valid_property_name { my $property_name = shift; return $property_name =~ m/^[_[:alpha:]][_[:alnum:]]*$/; } sub is_valid_class_name { my $class = shift; return $class =~ m/^[[:alpha:]]\w*((::|')\w+)*$/; } { my %subclass_suffix_for_builtin_symbolic_operator = ( '=' => "Equals", '<' => "LessThan", '>' => "GreaterThan", '[]' => "In", 'in []' => "In", 'ne' => "NotEquals", '<=' => 'LessOrEqual', '>=' => 'GreaterOrEqual', ); my %subclass_suffix_for_builtin_symbolic_operator_negation = ( '<' => 'GreaterOrEqual', # 'not less than' is the same as GreaterOrEqual '<=' => 'GreaterThan', '>' => 'LessOrEqual', '>=' => 'LessThan', 'ne' => 'Equals', 'false' => 'True', 'true' => 'False', ); sub class_suffix_for_operator { my $comparison_operator = shift; my $not = 0; if ($comparison_operator and $comparison_operator =~ m/^(\!|not)\s*(.*)/) { $not = 1; $comparison_operator = $2; } if (!defined($comparison_operator) or $comparison_operator eq '') { $comparison_operator = '='; } my $suffix; if ($not) { $suffix = $subclass_suffix_for_builtin_symbolic_operator_negation{$comparison_operator}; unless ($suffix) { $suffix = $subclass_suffix_for_builtin_symbolic_operator{$comparison_operator} || ucfirst(lc($comparison_operator)); $suffix = "Not$suffix"; } } else { $suffix = $subclass_suffix_for_builtin_symbolic_operator{$comparison_operator} || ucfirst(lc($comparison_operator)); } return $suffix; } } # From DBI::quote() # needed in a few places where we need to quote some SQL but don't # have access to a database handle to call quote() on sub sql_quote { my $str = shift; return "NULL" unless defined $str; $str =~ s/'/''/g; # ISO SQL2 return "'$str'"; } # Module::Runtime's use_package_optimistically will not throw an exception if # the package cannot be found or if it fails to compile but will if the package # has upstream exceptions, e.g. a missing dependency. We're a little less # "optimistic" so we check if the package is in %INC so we can report whether # it was believed to be loaded or not. sub use_package_optimistically { my $name = Module::Runtime::use_package_optimistically(shift); my $file = module_notional_filename($name); return $INC{$file}; } 1; =pod =head1 NAME UR::Util - Collection of utility subroutines and methods =head1 DESCRIPTION This package contains subroutines and methods used by other parts of the infrastructure. These subs are not likely to be useful to outside code. =cut ArrayRefIterator.pm100664023532023421 100212544604516 17422 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Utilpackage UR::Util::ArrayRefIterator; use strict; use warnings; use UR; class UR::Util::ArrayRefIterator { has => [ arrayref => { is => 'UR::Value::ARRAY', }, position => { is => 'Integer', is_optional => 1, default => 0, }, ], id_by => 'arrayref', }; sub next { my $self = shift; my @ar = @{$self->arrayref}; my $val = $ar[$self->position]; $self->position($self->position + 1); return $val; } 1; Value.pm100664023532023421 1150412544604516 14364 0ustar00abrummetgsc000000000000UR-0.44/lib/URpackage UR::Value; use strict; use warnings; require UR; use List::MoreUtils; our $VERSION = "0.44"; # UR $VERSION; our @CARP_NOT = qw( UR::Context ); UR::Object::Type->define( class_name => 'UR::Value', is => 'UR::Object', has => ['id'], data_source => 'UR::DataSource::Default', ); sub __display_name__ { return shift->id; } sub __load__ { my $class = shift; my $rule = shift; my $expected_headers = shift; my $class_meta = $class->__meta__; unless ($class_meta->{_value_loader}) { my @id_property_names = $class_meta->all_id_property_names; my %id_property_names = map { $_ => 1 } @id_property_names; my $loader = sub { my $bx = shift; my $id = $bx->value_for_id; unless (defined $id) { Carp::croak "Can't load an infinite set of " . $bx->subject_class_name . ". Some id properties were not specified in the rule $bx"; } my @rows; if (ref($id) and ref($id) eq 'ARRAY') { # Multiple IDs passed in - return rows for multiple objects my @non_id = grep { ! $id_property_names{$_} } $bx->template->_property_names; if (@non_id) { Carp::croak("Cannot load class " . $bx->subject_class_name . " via UR::DataSource::Default when 'id' is a listref and non-id" . " properties appear in the rule: " . join(', ', @non_id)); } # Get the 1st value from each list, then the second, then the third, etc my $iter = List::MoreUtils::each_arrayref map { my $v = $bx->value_for($_); (ref($v) eq 'ARRAY') ? $v : [ $v ] } @$expected_headers; while(my @row = $iter->()) { push @rows, \@row; } } else { # single ID - return a single row my @row = map { $bx->value_for($_) } @$expected_headers; @rows = ( \@row ); } return ($expected_headers, \@rows); }; $class_meta->{_value_loader} = $loader; } return $class_meta->{_value_loader}->($rule); } sub underlying_data_types { return (); } package UR::Value::Type; sub get_composite_id_decomposer { my $class_meta = shift; unless ($class_meta->{get_composite_id_decomposer}) { my @id_property_names = $class_meta->id_property_names; my $instance_class = $class_meta->class_name; if (my $decomposer = $instance_class->can('__deserialize_id__')) { $class_meta->{get_composite_id_decomposer} = sub { my @ids = (ref($_[0]) and ref($_[0]) eq 'ARRAY') ? @{$_[0]} : ( $_[0] ); my @retval; if (@ids == 1) { my $h = $instance_class->$decomposer($ids[0]); @retval = @$h{@id_property_names}; } else { # Get the 1st value from each list, then the second, then the third, etc my @decomposed = map { my $h = $instance_class->$decomposer($_); [ @$h{@id_property_names} ] } @ids; my $iter = List::MoreUtils::each_arrayref @decomposed; while( my @row = $iter->() ) { push @retval, \@row; } } return @retval; }; } else { $decomposer = $class_meta->SUPER::get_composite_id_decomposer(); $class_meta->{get_composite_id_decomposer} = $decomposer; } } return $class_meta->{get_composite_id_decomposer}; } sub get_composite_id_resolver { my $class_meta = shift; unless ($class_meta->{get_composite_id_resolver}) { my @id_property_names = $class_meta->id_property_names; my $instance_class = $class_meta->class_name; if (my $resolver = $instance_class->can('__serialize_id__')) { $class_meta->{get_composite_id_resolver} = sub { my %h = map { $_ => shift } @id_property_names; return $instance_class->__serialize_id__(\%h); }; } else { $resolver = $class_meta->SUPER::get_composite_id_resolver(); $class_meta->{get_composite_id_resolver} = $resolver; } } return $class_meta->{get_composite_id_resolver}; } 1; ARRAY.pm100664023532023421 34212544604516 15200 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::ARRAY; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::ARRAY', is => ['UR::Value::PerlReference'], ); 1; #$Header$ Blob.pm100664023532023421 32112544604516 15175 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Blob; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Blob', is => ['UR::Value'], ); 1; #$Header$ Boolean.pm100664023532023421 32212544604516 15677 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Boolean; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Boolean', is => ['UR::Value::Text'], ); 1; Text.pm100664023532023421 54112544604516 21124 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Value/Boolean/View/Defaultpackage UR::Value::Boolean::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; class UR::Value::Boolean::View::Default::Text { is => 'UR::Object::View::Default::Text' }; sub _generate_content { my $self = shift; my $subject = $self->subject(); return $subject && $subject->id ? "1" : "0"; } 1; CODE.pm100664023532023421 34012544604516 15032 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::CODE; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::CODE', is => ['UR::Value::PerlReference'], ); 1; #$Header$ CSV.pm100664023532023421 31712544604516 14757 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::CSV; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::CSV', is => ['UR::Value'], ); 1; #$Header$ DateTime.pm100664023532023421 33112544604516 16014 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::DateTime; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::DateTime', is => ['UR::Value'], ); 1; #$Header$ Decimal.pm100664023532023421 32312544604516 15657 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Decimal; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Decimal', is => ['UR::Value::Number'], ); 1; DirectoryPath.pm100664023532023421 35512544604516 17107 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::DirectoryPath; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::DirectoryPath', is => ['UR::Value::FilePath'], ); 1; #$Header$ FOF.pm100664023532023421 31712544604516 14736 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::FOF; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FOF', is => ['UR::Value'], ); 1; #$Header$ FilePath.pm100664023532023421 53012544604516 16015 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::FilePath; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FilePath', is => ['UR::Value::FilesystemPath'], ); sub line_count { my $self = shift; my ($line_count) = qx(wc -l $self) =~ /^\s*(\d+)/; return $line_count; } 1; FilesystemPath.pm100664023532023421 63212544604516 17265 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::FilesystemPath; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FilesystemPath', is => 'UR::Value::Text', ); sub exists { return -e shift; } sub is_dir { return -d shift; } sub is_file { return -f shift; } sub is_symlink { return -l shift; } sub size { return -s shift; } 1; Float.pm100664023532023421 31712544604516 15371 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Float; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Float', is => ['UR::Value::Number'], ); 1; GLOB.pm100664023532023421 34012544604516 15043 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::GLOB; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::GLOB', is => ['UR::Value::PerlReference'], ); 1; #$Header$ HASH.pm100664023532023421 237612544604516 15076 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::HASH; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::HASH', is => ['UR::Value::PerlReference'], ); sub __display_name__ { my $self = shift; my $hash = $self->id; my @values; for my $key (sort keys %$hash) { next unless defined $hash->{$key}; push @values, "$key => '".( defined $hash->{$key} ? $hash->{$key} : '' ). "'"; } my $join = ( defined $_[0] ) ? $_[0] : ','; # Default join is a comma return join($join, @values); } sub to_text { my $self = shift; my $hash = $self->id; my @tokens; for my $key (sort keys %$hash) { push @tokens, '-'.$key; next if not defined $hash->{$key} or $hash->{$key} eq ''; if ( my $ref = ref $hash->{$key} ) { if ( $ref ne 'ARRAY' ) { $self->warning_message("Can not convert hash to text. Cannot handle $ref for $key"); return; } push @tokens, @{$hash->{$key}}; } else { push @tokens, $hash->{$key}; } } my $join = ( defined $_[0] ) ? $_[0] : ' '; # Default join is a space return UR::Value::Text->get( join($join, @tokens)); } 1; Integer.pm100664023532023421 34012544604516 15715 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Integer; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Integer', is => ['UR::Value::Number'], ); 1; #$Header$ Iterator.pm100664023532023421 72412544604516 16117 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Iterator; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; sub create { my $class = shift; my $set = $class->define_set(@_); my @members = $set->members; return $class->create_for_value_arrayref(\@members); } sub create_for_value_arrayref { my ($class, $arrayref) = @_; my @copy = @$arrayref; return bless { members => \@copy }, $class; } sub next { shift @{ shift->{members} }; } 1; JSON.pm100664023532023421 47412544604516 15101 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::JSON; use strict; use warnings; use JSON; class UR::Value::JSON { is => 'UR::Value', }; my $_JS_CODEC = new JSON->allow_nonref; sub __serialize_id__ { shift; return $_JS_CODEC->canonical->encode(@_); } sub __deserialize_id__ { shift; return $_JS_CODEC->decode(@_); } 1; Number.pm100664023532023421 31112544604516 15546 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Number; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Number', is => ['UR::Value'], ); 1; PerlReference.pm100664023532023421 114612544604516 17066 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::PerlReference; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::PerlReference', is => ['UR::Value'], ); my %underlying_data_types; sub underlying_data_types { my $class = shift; my $class_name = ref($class) ? $class->class_name : $class; unless (exists $underlying_data_types{$class_name}) { my($base_type) = ($class_name =~ m/^UR::Value::(.*)/); $underlying_data_types{$class_name} = [$base_type]; } return @{$underlying_data_types{$class_name}}; } 1; #$Header$ REF.pm100664023532023421 33612544604516 14741 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::REF; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::REF', is => ['UR::Value::PerlReference'], ); 1; #$Header$ SCALAR.pm100664023532023421 34412544604516 15271 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::SCALAR; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::SCALAR', is => ['UR::Value::PerlReference'], ); 1; #$Header$ Set.pm100664023532023421 47012544604516 15057 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Set; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; sub members { my $self = shift; my %params = $self->rule->params_list; my $id = $params{id}; if (ref($id) eq 'ARRAY') { return (@$id); } else { return ($id); } } 1; SloppyPrimitive.pm100664023532023421 51712544604516 17505 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::SloppyPrimitive; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::SloppyPrimitive', is => ['UR::Value'], ); # namespaces which have allow_sloppy_primitives() set to true # will use this for any unrecognizable data types. 1; String.pm100664023532023421 33312544604516 15570 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::String; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::String', is => ['UR::Value::Text'], ); 1; #$Header$ Text.pm100664023532023421 507012544604516 15271 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Text; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Text', is => ['UR::Value'], ); use overload ( '.' => \&concat, '""' => \&stringify, fallback => 1, ); sub swap { my ($a, $b) = @_; return ($b, $a); } sub concat { my ($self, $other, $swap) = @_; my $class = ref $self; $self = $self->id; ($self, $other) = swap($self, $other) if $swap; return $class->get($self . $other); } sub stringify { my $self = shift; return $self->id; } sub capitalize { my $self = shift; my $seps = join('', ' ', @_); # allow other separators my $regexp = qr/[$seps]+/; my $capitalized_string = join(' ', map { ucfirst } split($regexp, $self->id)); return $self->class->get($capitalized_string); } sub to_camel { my $self = shift; my $seps = join('', ( @_ ? @_ : ( ' ', '_' ))); my $regexp = qr/[$seps]+/; my $camel_case = join('', map { ucfirst } split($regexp, $self->id)); return $self->class->get($camel_case); } sub to_lemac { # camel backwards = undo camel case. This was nutters idea. Ignore 'git blame' my $self = shift; # Split on the first capital or the start of a number my @words = split( /(?=(?id); # Default join is a space my $join = ( defined $_[0] ) ? $_[0] : ' '; return $self->class->get( join($join, map { lc } @words) ); } sub to_hash { my ($self, $split) = @_; # split splits to value of a key into many values my $text = $self->id; if ( $text !~ m#^-# ) { $self->warning_message('Can not convert text object with id "' . $self->id . '" to hash. Text must start with a dash (-)'); return; } my %hash; my @values = split(/\s?(\-{1,2}\D[\w\d\-]*)\s?/, $text); shift @values; for ( my $i = 0; $i < @values; $i += 2 ) { my $key = $values[$i]; $key =~ s/^\-{1,2}//; if ( $key eq '' ) { $self->warning_message("Can not convert text ($text) to hash. Found empty dash (-)."); return; } my $value = $values[$i + 1]; if ( defined $value ){ $value =~ s/\s*$//; } else { $value = ''; } # FIXME What if the key exists? if ( defined $split ) { $hash{$key} = [ split($split, $value) ]; } else { $hash{$key} = $value; } } #print Data::Dumper::Dumper(\@values, \%hash); return UR::Value::HASH->get(\%hash); } 1; Timestamp.pm100664023532023421 34312544604516 16266 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::Timestamp; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Timestamp', is => ['UR::Value::DateTime'], ); 1; #$Header$ URL.pm100664023532023421 32512544604516 14765 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Valuepackage UR::Value::URL; use strict; use warnings; require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::URL', is => ['UR::Value::Text'], ); 1; #$Header$ Html.pm100664023532023421 24512544604516 17526 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Html; use strict; use warnings; use UR; class UR::Value::View::Default::Html { is => 'UR::Value::View::Default::Text', }; 1; Json.pm100664023532023421 37212544604516 17534 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Json; use strict; use warnings; use UR; # These Values inherit from Text which inherits from UR::Object::View::Default::Text class UR::Value::View::Default::Json { is => 'UR::Value::View::Default::Text', }; 1; Text.pm100664023532023421 52412544604516 17546 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Text; use strict; use warnings; use UR; class UR::Value::View::Default::Text { is => 'UR::Object::View::Default::Text', }; sub _generate_content { my $self = shift; my $subject = $self->subject; return unless ($subject); my $name = $subject->__display_name__; return $name; } 1; Xml.pm100664023532023421 144612544604516 17406 0ustar00abrummetgsc000000000000UR-0.44/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Xml; use strict; use warnings; use UR; class UR::Value::View::Default::Xml { is => ['UR::Object::View::Default::Xml', 'UR::Value::View::Default::Text'], }; sub _generate_xml_doc { my $self = shift; my $xml_doc = $self->SUPER::_generate_xml_doc(@_); if ($self->subject_class_name->isa('UR::Value::PerlReference')) { $self->_add_perl_data_to_node($self->subject_id); } return $xml_doc; } sub _generate_content { my $self = shift; my $content; if ($self->subject_class_name->isa('UR::Value::PerlReference')) { $content = $self->UR::Object::View::Default::Xml::_generate_content(@_); } else { $content = $self->UR::Value::View::Default::Text::_generate_content(@_); } return $content; } 1; Vocabulary.pm100664023532023421 375612544604516 15411 0ustar00abrummetgsc000000000000UR-0.44/lib/UR package UR::Vocabulary; use strict; use warnings; use Lingua::EN::Inflect ("PL_V","PL"); require UR; our $VERSION = "0.44"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Vocabulary', is => ['UR::Singleton'], doc => 'A word in the vocabulary of a given namespace.', ); sub get_words_with_special_case { shift->_singleton_class_name->_words_with_special_case; } sub _words_with_special_case { return ('UR'); } sub convert_to_title_case { my $conversion_hashref = shift->_words_with_special_case_hashref; my @results; for my $word_in(@_) { my $word = lc($word_in); if (my $uc = $conversion_hashref->{$word}) { push @results, $uc; } else { push @results, ucfirst($word); } } return $results[0] if @results == 1 and !wantarray; return @results; } sub convert_to_special_case { my $conversion_hashref = shift->_words_with_special_case_hashref; my @results; for my $word_in(@_) { my $word = lc($word_in); if (my $sc = $conversion_hashref->{$word}) { push @results, $sc; } else { push @results, $word_in; } } return $results[0] if @results == 1 and !wantarray; return @results; } sub _words_with_special_case_hashref { my $self = shift->_singleton_object; my $hashref = $self->{_words_with_special_case_hashref}; return $hashref if $hashref; $hashref = { map { lc($_) => $_ } $self->get_words_with_special_case }; $self->{_words_with_special_case_hashref} = $hashref; return $hashref; } sub singular_to_plural { my $self = shift; return map { PL($_) } @_; } our %exceptions = ( statuses => 'status', is => 'is', has => 'has', cds => 'cds', ); sub plural_to_singular { my $self = shift; my ($lc,$override); return map { $lc = lc($_); $override = $exceptions{$lc}; ( $override ? $override : PL_V($_) ) } @_; } 1; above.pm100664023532023421 652512544604516 14045 0ustar00abrummetgsc000000000000UR-0.44/libpackage above; use strict; use warnings; use Cwd qw(getcwd); use File::Spec qw(); our $VERSION = '0.03'; # No BumpVersion sub import { my $package = shift; for (@_) { use_package($_); } } our %used_libs; BEGIN { %used_libs = ($ENV{PERL_USED_ABOVE} ? (map { $_ => 1 } split(":", $ENV{PERL_USED_ABOVE})) : ()); for my $path (keys %used_libs) { my $error = do { local $@; eval "use lib '$path';"; $@; }; die "Failed to use library path '$path' from the environment PERL_USED_ABOVE?: $error" if $error; } }; sub _caller_use { my ($caller, $class) = @_; my $error = do { local $@; eval "package $caller; use $class"; $@; }; die $error if $error; } sub _dev { my $path = shift; return (stat($path))[0]; } sub use_package { my $class = shift; my $caller = (caller(1))[0]; my $module = File::Spec->join(split(/::/, $class)) . '.pm'; ## paths already found in %used_above have ## higher priority than paths based on cwd for my $path (keys %used_libs) { if (-e File::Spec->join($path, $module)) { _caller_use($caller, $class); return; } } my $xdev = $ENV{ABOVE_DISCOVERY_ACROSS_FILESYSTEM}; my $cwd = getcwd(); unless ($cwd) { die "cwd failed: $!"; } my $dev = _dev($cwd); my $abort_crawl = sub { my @parts = @_; return 1 if (@parts == 1 && $parts[0] eq ''); # hit root dir my $path = File::Spec->join(@parts); return !($xdev || _dev($path) == $dev); # crossed device }; my $found_module_at = sub { my $path = shift; return (-e File::Spec->join($path, $module)); }; my @parts = File::Spec->splitdir($cwd); my $path; do { $path = File::Spec->join(@parts); pop @parts; } until ($found_module_at->($path) || $abort_crawl->(@parts)); if ($found_module_at->($path)) { while ($path =~ s:/[^/]+/\.\./:/:) { 1 } # simplify unless ($used_libs{$path}) { print STDERR "Using libraries at $path\n" unless $ENV{PERL_ABOVE_QUIET} or $ENV{COMP_LINE}; my $error = do { local $@; eval "use lib '$path';"; $@; }; die $error if $error; $used_libs{$path} = 1; my $env_value = join(":", sort keys %used_libs); $ENV{PERL_USED_ABOVE} = $env_value; } } _caller_use($caller, $class); }; 1; =pod =head1 NAME above - auto "use lib" when a module is in the tree of the PWD =head1 SYNOPSIS use above "My::Module"; =head1 DESCRIPTION Used by the command-line wrappers for Command modules which are developer tools. Do NOT use this in modules, or user applications. Uses a module as though the cwd and each of its parent directories were at the beginnig of @INC. If found in that path, the parent directory is kept as though by "use lib". Set ABOVE_DISCOVERY_ACROSS_FILESYSTEM shell variable to true value to crawl past device boundaries. =head1 EXAMPLES # given /home/me/perlsrc/My/Module.pm # in /home/me/perlsrc/My/Module/Some/Path/ # in myapp.pl: use above "My::Module"; # does this ..if run anywhere under /home/me/perlsrc: use lib '/home/me/perlsrc/' use My::Module; =head1 AUTHOR Scott Smith Nathaniel Nutter =cut minil.toml100664023532023421 10612544604516 13617 0ustar00abrummetgsc000000000000UR-0.44name = "UR" badges = ["travis"] [build] build_class = "builder::UR" ur-define-class.pod100664023532023421 130312544604516 16101 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define class - Add one or more classes to the current namespace =head1 VERSION This document describes ur define class version 0.29. =head1 SYNOPSIS ur define class --extends=? [NAMES] $ cd Acme $ ur define class Animal Vegetable Mineral A Acme::Animal A Acme::Vegetable A Acme::Mineral $ ur define class Dog Cat Bird --extends Animal A Acme::Dog A Acme::Cat A Acme::Bird =head1 REQUIRED ARGUMENTS =over =item extends The base class. Defaults to UR::Object. Default value 'UR::Object' if not specified =back =head1 OPTIONAL ARGUMENTS =over =item NAMES (undocumented) =back =head1 DESCRIPTION: Add one or more classes to the current namespace =cut ur-define-datasource-file.pod100664023532023421 165712544604516 20057 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource file - Add a file-based data source (not yet implemented) =head1 VERSION This document describes ur define datasource file version 0.29. =head1 SYNOPSIS ur define datasource file --server=? [--singleton] [--dsid=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item server I "server" attribute for this data source, such as a database name =item singleton I by default all data sources are singletons, but this can be turned off Default value 'true' if not specified =item nosingleton I Make singleton 'false' =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a file-based data source (not yet implemented) =cut ur-define-datasource-mysql.pod100664023532023421 220712544604516 20275 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource mysql - Add a MySQL data source to the current namespace. =head1 VERSION This document describes ur define datasource mysql version 0.29. =head1 SYNOPSIS ur define datasource mysql --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a MySQL data source to the current namespace. =cut ur-define-datasource-oracle.pod100664023532023421 221612544604516 20375 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource oracle - Add an Oracle data source to the current namespace. =head1 VERSION This document describes ur define datasource oracle version 0.29. =head1 SYNOPSIS ur define datasource oracle --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add an Oracle data source to the current namespace. =cut ur-define-datasource-pg.pod100664023532023421 221012544604516 17530 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource pg - Add a PostgreSQL data source to the current namespace. =head1 VERSION This document describes ur define datasource pg version 0.29. =head1 SYNOPSIS ur define datasource pg --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a PostgreSQL data source to the current namespace. =cut ur-define-datasource-sqlite.pod100664023532023421 240712544604516 20433 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource sqlite - Add a SQLite data source to the current namespace. =head1 VERSION This document describes ur define datasource sqlite version 0.29. =head1 SYNOPSIS ur define datasource sqlite [--nosingleton] [--dsid=?] [--server=?] [DSNAME] cd Acme ur define datasource sqlite --dsname MyDB1 # writes Acme::DataSource::MyDB1 to work with Acme/DataSource/MyDB1.sqlite3 ur define datasource sqlite --dsname MyDB2 --server /var/lib/acmeapp/mydb2.sqlite3 # writes Acme::DataSource::MyDB2 to work with the specified sqlite file =head1 REQUIRED ARGUMENTS =over =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a SQLite data source to the current namespace. =cut ur-define-datasource.pod100664023532023421 107412544604516 17133 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define datasource - add a data source to the current namespace =head1 VERSION This document describes ur define datasource version 0.29. =head1 SYNOPSIS ur define datasource [file|mysql|oracle|pg|sqlite] ... =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: add a data source to the current namespace =cut ur-define-db.pod100664023532023421 165112544604516 15367 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define db - add a data source to the current namespace =head1 VERSION This document describes ur define db version 0.29. =head1 SYNOPSIS ur define db URI NAME ur define db dbi:SQLite:/some/file.db Db1 ur define db me@dbi:mysql:myserver MainDb ur define db me@dbi:Oracle:someserver ProdDb ur define db me@dbi:Oracle:someserver~schemaname BigDb ur define db me@dbi:Pg:prod Db1 ur define db me@dbi:Pg:dev Testing::Db1 # alternate for "Testing" (arbitrary) context ur define db me@dbi:Pg:stage Staging::Db1 # alternate for "Staging" (arbitrary) context =head1 REQUIRED ARGUMENTS =over =item URI I a DBI connect string like dbi:mysql:someserver or user/passwd@dbi:Oracle:someserver~defaultns =item NAME I the name for this data source (used for class naming) Default value 'Db1' if not specified =back =head1 DESCRIPTION: add a data source to the current namespace =cut ur-define-namespace.pod100664023532023421 62212544604516 16713 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define namespace - create a new namespace tree and top-level module =head1 VERSION This document describes ur define namespace version 0.29. =head1 SYNOPSIS ur define namespace NSNAME =head1 REQUIRED ARGUMENTS =over =item NSNAME the name of the namespace, and first "word" in all classes =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-define.pod100664023532023421 65112544604516 14763 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur define - define namespaces, data sources and classes =head1 VERSION This document describes ur define version 0.29. =head1 SUB-COMMANDS namespace NSNAME create a new namespace tree and top-level module db URI NAME add a data source to the current namespace class --extends=? [NAMES] Add one or more classes to the current namespace =cut ur-describe.pod100664023532023421 75012544604516 15311 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur describe - show class properties, relationships, meta-data =head1 VERSION This document describes ur describe version 0.29. =head1 SYNOPSIS ur describe CLASSES-OR-MODULES ur describe UR::Object ur describe Acme::Order Acme::Product Acme::Order::LineItem =head1 REQUIRED ARGUMENTS =over =item CLASSES-OR-MODULES classes to describe by class name or module path =back =head1 DESCRIPTION: show class properties, relationships, meta-data =cut ur-init.pod100664023532023421 76012544604516 14475 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur init - create a new ur app with default classes in place =head1 VERSION This document describes ur init version 0.29. =head1 SYNOPSIS ur init NAMESPACE [DB] =head1 REQUIRED ARGUMENTS =over =item NAMESPACE I the name of the namespace/app to create =back =head1 OPTIONAL ARGUMENTS =over =item DB I the (optional) DBI connection string for the primary data source =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-list-classes.pod100664023532023421 65712544604516 16145 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur list classes - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur list classes version 0.29. =head1 SYNOPSIS ur list classes [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-list-modules.pod100664023532023421 65712544604516 16160 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur list modules - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur list modules version 0.29. =head1 SYNOPSIS ur list modules [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-list-objects.pod100664023532023421 442412544604516 16155 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur list objects - lists objects matching specified params =head1 VERSION This document describes ur list objects version 0.29. =head1 SYNOPSIS ur list objects --subject-class-name=? [--csv-delimiter=?] [--filter=?] [--noheaders] [--show=?] [--style=?] =head1 REQUIRED ARGUMENTS =over =item subject-class-name I (undocumented) =back =head1 OPTIONAL ARGUMENTS =over =item csv-delimiter I For the csv output style, specify the field delimiter Default value ',' if not specified =item filter I Filter results based on the parameters. See below for how to. =item noheaders I Do not include headers Default value 'false' (--nonoheaders) if not specified =item nonoheaders I Make noheaders 'false' =item show I Specify which columns to show, in order. =item style I Style of the list: text (default), csv, pretty, html, xml Default value 'text' if not specified =back =head1 DESCRIPTION: Listing Styles: --------------- text - table like csv - comma separated values pretty - objects listed singly with color enhancements html - html table xml - xml document using elements Filtering: ---------- Create filter equations by combining filterable properties with operators and values. Combine and separate these 'equations' by commas. Use single quotes (') to contain values with spaces: name='genome institute' Use percent signs (%) as wild cards in like (~). Use backslash or single quotes to escape characters which have special meaning to the shell such as < > and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- lister-command --filter name=Bob --show id,name,address lister-command --filter name='something with space',employees>200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe Filterable Properties: ---------------------- Can't determine the list of filterable properties without a subject_class_name =cut ur-list.pod100664023532023421 111012544604516 14513 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur list - list objects, classes, modules =head1 VERSION This document describes ur list version 0.29. =head1 SUB-COMMANDS objects --subject-class-n... lists objects matching specified params classes [CLASSES-OR-MODULES] a command which operates on classes/modules in a UR namespace directory modules [CLASSES-OR-MODULES] a command which operates on classes/modules in a UR namespace directory =cut ur-old-diff-rewrite.pod100664023532023421 57212544604516 16676 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old diff-rewrite - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur old diff-rewrite version 0.29. =head1 SYNOPSIS ur old diff-rewrite (no execute or sub commands implemented) =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-old-diff-update.pod100664023532023421 56712544604516 16503 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old diff-update - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur old diff-update version 0.29. =head1 SYNOPSIS ur old diff-update (no execute or sub commands implemented) =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-old-export-dbic-classes.pod100664023532023421 114312544604516 20175 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old export-dbic-classes - Create or update a DBIx::Class class from an already existing UR class =head1 VERSION This document describes ur old export-dbic-classes version 0.29. =head1 SYNOPSIS ur old export-dbic-classes [BARE-ARGS] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item BARE-ARGS (undocumented) =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Given one or more UR class names on the command line, this will create or update a DBIx::Class class. The files will appear under the DBIx directory in the namespace. =cut ur-old-info.pod100664023532023421 62312544604516 15237 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old info - Outputs description(s) of UR entities such as classes and tables to stdout =head1 VERSION This document describes ur old info version 0.29. =head1 SYNOPSIS ur old info [SUBJECT] =head1 OPTIONAL ARGUMENTS =over =item SUBJECT (undocumented) =back =head1 DESCRIPTION: Outputs description(s) of UR entities such as classes and tables to stdout =cut ur-old-redescribe.pod100664023532023421 64312544604516 16415 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old redescribe - Outputs class description(s) formatted to the latest standard. =head1 VERSION This document describes ur old redescribe version 0.29. =head1 SYNOPSIS ur old redescribe [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Outputs class description(s) formatted to the latest standard. =cut ur-old.pod100664023532023421 226012544604516 14325 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur old - define namespaces, data sources and classes =head1 VERSION This document describes ur old version 0.29. =head1 SUB-COMMANDS diff-rewrite (no execute or su... a command which operates on classes/modules in a UR namespace directory diff-update (no execute or su... a command which operates on classes/modules in a UR namespace directory export-dbic-classes [BARE-ARGS] [CLAS... Create or update a DBIx::Class class from an already existing UR class info [SUBJECT] Outputs description(s) of UR entities such as classes and tables to stdout redescribe [CLASSES-OR-MODULES] Outputs class description(s) formatted to the latest standard. =cut ur-sys-class-browser.pod100664023532023421 52512544604516 17133 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur sys class-browser - Start a web server to browse through the class and database structures. =head1 VERSION This document describes ur sys class-browser version 0.29. =head1 SYNOPSIS ur sys class-browser =head1 DESCRIPTION: Start a web server to browse through the class and database structures. =cut ur-sys.pod100664023532023421 46012544604516 14345 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur sys - service launchers =head1 VERSION This document describes ur sys version 0.29. =head1 SUB-COMMANDS class-browser Start a web server to browse through the class and database structures. =cut ur-test-callcount-list.pod100664023532023421 467312544604516 17473 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test callcount list - Filter and list Callcount items =head1 VERSION This document describes ur test callcount list version 0.29. =head1 SYNOPSIS ur test callcount list --file=? --show=? [--csv-delimiter=?] [--filter=?] [--noheaders] [--style=?] =head1 REQUIRED ARGUMENTS =over =item file I Specify the .callcount file Default value '/dev/null' if not specified =item show Specify which columns to show, in order. Default value 'count,subname,subloc,callers' if not specified =back =head1 OPTIONAL ARGUMENTS =over =item csv-delimiter I For the csv output style, specify the field delimiter Default value ',' if not specified =item filter I Filter results based on the parameters. See below for how to. =item noheaders I Do not include headers Default value 'false' (--nonoheaders) if not specified =item nonoheaders I Make noheaders 'false' =item style I Style of the list: text (default), csv, pretty, html, xml Default value 'text' if not specified =back =head1 DESCRIPTION: Listing Styles: --------------- text - table like csv - comma separated values pretty - objects listed singly with color enhancements html - html table xml - xml document using elements Filtering: ---------- Create filter equations by combining filterable properties with operators and values. Combine and separate these 'equations' by commas. Use single quotes (') to contain values with spaces: name='genome institute' Use percent signs (%) as wild cards in like (~). Use backslash or single quotes to escape characters which have special meaning to the shell such as < > and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- lister-command --filter name=Bob --show id,name,address lister-command --filter name='something with space',employees>200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe Filterable Properties: ---------------------- callers (String): (undocumented) count (Integer): (undocumented) subloc (String): (undocumented) subname (String): (undocumented) =cut ur-test-callcount.pod100664023532023421 45012544604516 16467 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test callcount - Collect the data from a prior 'ur test run --callcount' run into a single output file =head1 VERSION This document describes ur test callcount version 0.29. =head1 SUB-COMMANDS list --file=? --show=?... Filter and list Callcount items =cut ur-test-compile.pod100664023532023421 133512544604516 16156 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test compile - Attempts to compile each module in the namespace in its own process. =head1 VERSION This document describes ur test compile version 0.29. =head1 SYNOPSIS ur test compile [CLASSES-OR-MODULES] ur test complie ur test compile Some::Module Some::Other::Module ur test complile Some/Module.pm Some/Other/Mod*.pm =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: This command runs "perl -c" on each module in a separate process and aggregates results. Running with --verbose will list specific modules instead of just a summary. Try "ur test use" for a faster evaluation of whether your software tree is broken. :) =cut ur-test-eval.pod100664023532023421 123512544604516 15454 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test eval - Evaluate a string of Perl source =head1 VERSION This document describes ur test eval version 0.29. =head1 SYNOPSIS ur test eval [BARE-ARGS] ur test eval 'print "hello\n"' ur test eval 'print "hello\n"' 'print "goodbye\n"' ur test eval 'print "Testing in the " . \$self->namespace_name . " namespace.\n"' =head1 OPTIONAL ARGUMENTS =over =item BARE-ARGS (undocumented) =back =head1 DESCRIPTION: This command is for testing and debugging. It simply eval's the Perl source supplied on the command line, after using the current namespace. A $self object is in scope representing the current context. =cut ur-test-run.pod100664023532023421 706312544604516 15336 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test run - Run the test suite against the source tree. =head1 VERSION This document describes ur test run version 0.29. =head1 SYNOPSIS ur test run [--color] [--junit] [--list] [--lsf] [--recurse] [--callcount] [--cover=?] [--cover-cvs-changes] [--cover-svk-changes] [--cover-svn-changes] [--coverage] [--inc=?[,?]] [--jobs=?] [--long] [--lsf-params=?] [--noisy] [--perl-opts=?] [--run-as-lsf-helper=?] [--script-opts=?] [--time=?] [BARE-ARGS] cd MyNamespace ur test run --recurse # run all tests in the namespace ur test run # runs all tests in the t/ directory under pwd ur test run t/mytest1.t My/Class.t # run specific tests ur test run -v -t --cover-svk-changes # run tests to cover latest svk updates ur test run -I ../some/path/ # Adds ../some/path to perl's @INC through -I ur test run --junit # writes test output in junit's xml format (consumable by Hudson integration system) =head1 REQUIRED ARGUMENTS =over =item color I Use TAP::Harness::Color to generate color output Default value 'false' (--nocolor) if not specified =item nocolor I Make color 'false' =item junit I Run all tests with junit style XML output. (requires TAP::Formatter::JUnit) =item nojunit I Make junit 'false' =item list I List the tests, but do not actually run them. =item nolist I Make list 'false' =item lsf I If true, tests will be submitted as jobs via bsub =item nolsf I Make lsf 'false' =item recurse I Run all .t files in the current directory, and in recursive subdirectories. =item norecurse I Make recurse 'false' =back =head1 OPTIONAL ARGUMENTS =over =item callcount I Count the number of calls to each subroutine/method =item nocallcount I Make callcount 'false' =item cover I Cover only this(these) modules =item cover-cvs-changes I Cover modules modified in cvs status =item nocover-cvs-changes I Make cover-cvs-changes 'false' =item cover-svk-changes I Cover modules modified in svk status =item nocover-svk-changes I Make cover-svk-changes 'false' =item cover-svn-changes I Cover modules modified in svn status =item nocover-svn-changes I Make cover-svn-changes 'false' =item coverage I Invoke Devel::Cover =item nocoverage I Make coverage 'false' =item inc I Additional paths for @INC, alias for -I =item jobs I How many tests to run in parallel Default value '1' if not specified =item long I Run tests including those flagged as long =item nolong I Make long 'false' =item lsf-params I Params passed to bsub while submitting jobs to lsf Default value '-q short -R select[type==LINUX64]' if not specified =item noisy I doesn't redirect stdout =item nonoisy I Make noisy 'false' =item perl-opts I Override options to the Perl interpreter when running the tests (-d:Profile, etc.) Default value '' if not specified =item run-as-lsf-helper I Used internally by the test harness =item script-opts I Override options to the test case when running the tests (--dump-sql --no-commit) Default value '' if not specified =item time I Write timelog sum to specified file =item BARE-ARGS (undocumented) =back =head1 DESCRIPTION: This command is like "prove" or "make test", running the test suite for the current namespace. =cut ur-test-track-object-release.pod100664023532023421 147312544604516 20517 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test track-object-release - Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks =head1 VERSION This document describes ur test track-object-release version 0.29. =head1 SYNOPSIS ur test track-object-release --file=? ur test track-object-release --file /path/to/text.file > /path/to/results =head1 REQUIRED ARGUMENTS =over =item file I pathname of the input file =back =head1 DESCRIPTION: When a UR-based program is run with the UR_DEBUG_OBJECT_RELEASE environment variable set to 1, it will emit messages to STDERR describing the various stages of releasing an object. This command parses those messages and provides a report on objects which did not completely deallocate themselves, usually because of a reference being held. =cut ur-test-use.pod100664023532023421 303512544604516 15321 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test use - Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to @INC by any modules (bad!). =head1 VERSION This document describes ur test use version 0.29. =head1 SYNOPSIS ur test use [--exec=?] [--summarize-externals] [--verbose] [CLASSES-OR-MODULES] ur test use ur test use Some::Module Some::Other::Module ur test use ./Module.pm Other/Module.pm =head1 OPTIONAL ARGUMENTS =over =item exec I Execute the specified Perl _after_ using all of the modules. =item summarize-externals I List all modules used which are outside the namespace. =item nosummarize-externals I Make summarize-externals 'false' =item verbose I List each explicitly. =item noverbose I Make verbose 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Tests each module by "use"-ing it. Failures are reported individually. Successes are only reported individualy if the --verbose option is specified. A count of total successes/failures is returned as a summary in all cases. This command requires that the current working directory be under a namespace module. If no modules or class names are specified as parameters, it runs on all modules in the namespace. If modules or class names ARE listed, it will operate only on those. Words containing double-colons will be interpreted as absolute class names. All other words will be interpreted as relative file paths to modules. =cut ur-test-window.pod100664023532023421 46112544604516 16014 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test window - repl tk window =head1 VERSION This document describes ur test window version 0.29. =head1 SYNOPSIS ur test window [SRC] =head1 OPTIONAL ARGUMENTS =over =item SRC (undocumented) =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-test.pod100664023532023421 245612544604516 14535 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur test - tools for testing and debugging =head1 VERSION This document describes ur test version 0.29. =head1 SUB-COMMANDS callcount ... Collect the data from a prior 'ur test run --callcount' run into a single output file compile [CLASSES-OR-MODULES] Attempts to compile each module in the namespace in its own process. eval [BARE-ARGS] Evaluate a string of Perl source run [--color] [--juni... Run the test suite against the source tree. track-object-release --file=? Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks use [--exec=?] [--sum... Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to @INC by any modules (bad!). window [SRC] repl tk window =cut ur-update-class-diagram.pod100664023532023421 332412544604516 17540 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update class-diagram - Update an Umlet diagram based on the current class definitions =head1 VERSION This document describes ur update class-diagram version 0.29. =head1 SYNOPSIS ur update class-diagram --file=? [--data-source=?] [--depth=?] [--include-ur-object] [--show-attributes] [--show-methods] [INITIAL-NAME] =head1 REQUIRED ARGUMENTS =over =item file I Pathname of the Umlet (.uxf) file =back =head1 OPTIONAL ARGUMENTS =over =item data-source I Which datasource to use =item depth I Max distance of related classes to include. Default is 1. 0 means show only the named class(es), -1 means to include everything =item include-ur-object I Include UR::Object and UR::Entity in the diagram (default = no) Default value 'false' (--noinclude-ur-object) if not specified =item noinclude-ur-object I Make include-ur-object 'false' =item show-attributes I Include class attributes in the diagram Default value 'true' if not specified =item noshow-attributes I Make show-attributes 'false' =item show-methods I Include methods in the diagram (not implemented yet Default value 'false' (--noshow-methods) if not specified =item noshow-methods I Make show-methods 'false' =item INITIAL-NAME (undocumented) =back =head1 DESCRIPTION: Creates a new Umlet diagram, or updates an existing diagram. Bare arguments are taken as class names to include in the diagram. Other classes may be included in the diagram based on their distance from the names classes and the --depth parameter. If an existing file is being updated, the position of existing elements will not change. =cut ur-update-classes-from-db.pod100664023532023421 352112544604516 20011 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update classes-from-db - Update class definitions (and data dictionary cache) to reflect changes in the database schema. =head1 VERSION This document describes ur update classes-from-db version 0.29. =head1 SYNOPSIS ur update classes-from-db [--class-name=?] [--data-source=?] [--force-check-all-tables] [--force-rewrite-all-classes] [--table-name=?] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item class-name I Update only the specified classes. =item data-source I Limit updates to these data sources =item force-check-all-tables I By default we only look at tables with a new DDL time for changed database schema information. This explicitly (slowly) checks each table against our cache. =item noforce-check-all-tables I Make force-check-all-tables 'false' =item force-rewrite-all-classes I By default we only rewrite classes where there are database changes. Set this flag to rewrite all classes even where there are no schema changes. =item noforce-rewrite-all-classes I Make force-rewrite-all-classes 'false' =item table-name I Update the specified table. =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Reads from the data sources in the current working directory's namespace, and updates the local class tree. This hits the data dictionary for the remote database, and gets changes there first. Those changes are then used to mutate the class tree. If specific data sources are specified on the command-line, it will limit its database examination to just data in those data sources. This command will, however, always load ALL classes in the namespace when doing this update, to find classes which currently reference the updated table, or are connected to its class indirectly. =cut ur-update-pod.pod100664023532023421 175112544604516 15615 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update pod - generate man-page-like POD for a commands =head1 VERSION This document describes ur update pod version 0.29. (built on 2011-03-07 at 10:34:30) =head1 SYNOPSIS ur update pod [--input-path=?] [--output-path=?] EXECUTABLE-NAME CLASS-NAME TARGETS ur update pod -i ./lib -o ./pod ur UR::Namespace::Command =head1 REQUIRED ARGUMENTS =over =item EXECUTABLE-NAME I the name of the executable to document =item CLASS-NAME I the command class which maps to the executable =item TARGETS I specific classes to document (documents all unless specified) =back =head1 OPTIONAL ARGUMENTS =over =item input-path I optional location of the modules to document =item output-path I optional location to output .pod files =back =head1 DESCRIPTION: This tool generates POD documentation for each all of the commands in a tree for a given executable. This command must be run from within the namespace directory. =cut ur-update-rename-class.pod100664023532023421 156012544604516 17403 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update rename-class - Update::RewriteClassHeaders class descriptions headers to normalize manual changes. =head1 VERSION This document describes ur update rename-class version 0.29. =head1 SYNOPSIS ur update rename-class [--force] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item force I (undocumented) =item noforce I Make force 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. =cut ur-update-rewrite-class-header.pod100664023532023421 161012544604516 21037 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update rewrite-class-header - Update::RewriteClassHeaders class descriptions headers to normalize manual changes. =head1 VERSION This document describes ur update rewrite-class-header version 0.29. =head1 SYNOPSIS ur update rewrite-class-header [--force] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item force I (undocumented) =item noforce I Make force 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. =cut ur-update-schema-diagram.pod100664023532023421 232512544604516 17673 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update schema-diagram - Update an Umlet diagram based on the current schema =head1 VERSION This document describes ur update schema-diagram version 0.29. =head1 SYNOPSIS ur update schema-diagram --file=? [--data-source=?] [--depth=?] [--show-columns] [INITIAL-NAME] =head1 REQUIRED ARGUMENTS =over =item file I Pathname of the Umlet (.uxf) file =back =head1 OPTIONAL ARGUMENTS =over =item data-source I Which datasource to use =item depth I Max distance of related tables to include. Default is 1. 0 means show only the named tables, -1 means to include everything =item show-columns I Include column names in the diagram Default value 'true' if not specified =item noshow-columns I Make show-columns 'false' =item INITIAL-NAME (undocumented) =back =head1 DESCRIPTION: Creates a new Umlet diagram, or updates an existing diagram. Bare arguments are taken as table names to include in the diagram. Other tables may be included in the diagram based on their distance from the names tables and the --depth parameter. If an existing file is being updated, the position of existing elements will not change. =cut ur-update-tab-completion-spec.pod100664023532023421 125712544604516 20701 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update tab-completion-spec - Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =head1 VERSION This document describes ur update tab-completion-spec version 0.29. =head1 SYNOPSIS ur update tab-completion-spec [--output=?] CLASSNAME =head1 REQUIRED ARGUMENTS =over =item CLASSNAME I The base class to use as trunk of command tree, e.g. UR::Namespace::Command =back =head1 OPTIONAL ARGUMENTS =over =item output I Override output location of the opts spec file. =back =head1 DESCRIPTION: Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =cut ur-update.pod100664023532023421 262612544604516 15037 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur update - update parts of the source tree of a UR namespace =head1 VERSION This document describes ur update version 0.29. =head1 SUB-COMMANDS classes-from-db [--class-name=?] ... Update class definitions (and data dictionary cache) to reflect changes in the database schema. schema-diagram --file=? [--data-... Update an Umlet diagram based on the current schema class-diagram --file=? [--data-... Update an Umlet diagram based on the current class definitions pod [--input-path=?] ... generate man-page-like POD for a commands rename-class [--force] [CLASSE... Update::RewriteClassHeaders class descriptions headers to normalize manual changes. rewrite-class-header [--force] [CLASSE... Update::RewriteClassHeaders class descriptions headers to normalize manual changes. tab-completion-spec [--output=?] CLAS... Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =cut ur.pod100664023532023421 134112544604516 13550 0ustar00abrummetgsc000000000000UR-0.44/pod =pod =head1 NAME ur - tools to create and maintain a ur class tree =head1 VERSION This document describes ur version 0.29. =head1 SUB-COMMANDS init NAMESPACE [DB] create a new ur app with default classes in place define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging =cut CdExample.pm100664023532023421 15212544604516 14256 0ustar00abrummetgsc000000000000UR-0.44/tpackage CdExample; use strict; use warnings; use UR; class CdExample { is => 'UR::Namespace' }; 1; Artist.pm100664023532023421 67612544604516 15537 0ustar00abrummetgsc000000000000UR-0.44/t/CdExample package CdExample::Artist; use strict; use warnings; use CdExample; class CdExample::Artist { id_by => 'artist_id', has => [ name => { is => 'Text' }, cds => { is => 'CdExample::Cd', is_many => 1, reverse_as => 'artist' }, foo => { is => 'Text' }, #bar => { is => 'Text' }, baz => { is => 'Text' }, ], data_source => 'CdExample::DataSource::DB1', table_name => 'ARTISTS', }; 1; Cd.pm100664023532023421 64612544604516 14614 0ustar00abrummetgsc000000000000UR-0.44/t/CdExamplepackage CdExample::Cd; use strict; use warnings; use CdExample; class CdExample::Cd { id_by => 'cd_id', has => [ artist => { is => 'CdExample::Artist', id_by => 'artist_id' }, title => { is => 'Text' }, year => { is => 'Integer' }, artist_name => { via => 'artist', to => 'name' }, ], data_source => 'CdExample::DataSource::DB1', table_name => 'CDS', }; 1; CmdTest.pm100775023532023421 41712544604516 13766 0ustar00abrummetgsc000000000000UR-0.44/t#!/usr/bin/env perl package CmdTest; use strict; use warnings; use Command::Tree; class CmdTest { is => 'Command::Tree', doc => 'test suite test command tree' }; if ($0 eq __FILE__) { require Command::Shell; exit Command::Shell->run(__PACKAGE__,@ARGV); } 1; C1.pm100775023532023421 212312544604516 14245 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTestuse Command::V2; use strict; use warnings; package CmdTest::C1; use CmdTest::Stuff; class CmdTest::C1 { is => 'Command::V2', has_optional_input => [ z => { is => "Text" }, a => { is => "Text" }, b20 => { is => "Text" }, b3 => { is => "Text" }, ], has_optional_param => [ p3 => { is => 'Number' }, p1 => { is => 'Number' }, p2 => { is => 'Number' }, ], has_input => [ rz => { is => "Text" }, ra => { is => "Text" }, rb20 => { is => "Text" }, rb3 => { is => "Text" }, ], has_param => [ rp3 => { is => 'Number' }, rp1 => { is => 'Number' }, rp2 => { is => 'Number' }, ], has_output => [ #stuff => { is => 'CmdTest::Stuff' }, more => { is => 'Text' } ], doc => "test command 1" }; sub execute { my $self = shift; print "running $self with args: " . Data::Dumper::Dumper($self) . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; C2.pm100775023532023421 131512544604516 14250 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTestuse Command::V2; use strict; use warnings; package CmdTest::C2; use CmdTest::Stuff; class CmdTest::Thing { has => [ name => { is => 'Text' } ] }; CmdTest::Thing->create(id => 111, name => 'one'); CmdTest::Thing->create(id => 222, name => 'two'); CmdTest::Thing->create(id => 333, name => 'three'); class CmdTest::C2 { is => 'Command::V2', has => [ thing => { is => 'CmdTest::Thing', id_by => 'thing_id' }, ], doc => "test command 2" }; sub execute { my $self = shift; print "running $self with args: " . Data::Dumper::Dumper($self) . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; C3.pm100775023532023421 75312544604516 14236 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTestpackage CmdTest::C3; use Command::V2; use strict; use warnings; use CmdTest::C2; class CmdTest::C3 { is => ['CmdTest::C2'], has => [ thing_name => { is => 'Text', via => 'thing', to => 'name' }, ], doc => "test command 3" }; sub execute { my $self = shift; no warnings; print "thing_id is " . $self->thing_id . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; Stuff.pm100664023532023421 20612544604516 15046 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTestpackage CmdTest::Stuff; use strict; use warnings; class CmdTest::Stuff { has => [ foo => { is => "Text" }, ] }; 1; 01-mutual-resolution-via-to.t100664023532023421 167512544604516 21227 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTest/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 5; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Command::Shell; use CmdTest; use CmdTest::C2; use CmdTest::C3; # Put this into Perl5Lib so when we exec the commands below, they can # find CmdTest::Stuff $ENV{PERL5LIB} .= ':' . File::Basename::dirname(__FILE__)."/../.."; ok(CmdTest->isa('Command::Tree'), "CmdTest isa Command::Tree"); use_ok("CmdTest::C3"); my $path = $INC{"CmdTest/C3.pm"}; ok($path, "found path to test module") or die "cannot continue!"; my $result1 = `$^X $path --thing=two`; chomp $result1; is($result1, "thing_id is 222", "specifying an object automatically specifies its indirect value"); my $result2 = `$^X $path --thing-name=two`; chomp $result2; is($result2, "thing_id is 222", "specifying an indirect value automatically sets the value it is via"); 02-example-values.t100664023532023421 245012544604516 17243 0ustar00abrummetgsc000000000000UR-0.44/t/CmdTest/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Sub::Install; class Foo::TestCmd1 { is => 'Command::V1', has => [ arg1 => { is => 'Text', doc => 'first arg', example_values => [q(foo bar baz)] }, execute => { is => 'Integer' }, # HACK to get around needing an execute() method ], }; my $text = Foo::TestCmd1->help_usage_complete_text; $text =~ s/\e\[\d+(?>(;\d+)*)m//g; # Strip out ANSI escape sequences like($text, qr(arg1\s+Text.*?first arg.*?example:.*?foo bar baz)s, "arg1 has example values for Foo::TestCmd1"); class Foo::TestCmd2 { is => 'Command::V2', has => [ arg1 => { is => 'Text', doc => 'first arg', example_values => [q(foo bar baz)] }, # execute => { is => 'Integer' }, # HACK to get around needing an execute() method ], }; $text = Foo::TestCmd2->help_usage_complete_text; $text =~ s/\e\[\d+(?>(;\d+)*)m//g; # Strip out ANSI escape sequences # This regex differs from the above in that there's no type info after the arg name # in Command::V2 output like($text, qr(arg1.*?first arg.*?example:.*?foo bar baz)s, "arg1 has example values for Foo::TestCmd2"); Slimspace.pm100664023532023421 25112544604516 14334 0ustar00abrummetgsc000000000000UR-0.44/tpackage Vending; use warnings; use strict; use UR; class Vending { is => [ 'UR::Namespace' ], doc => 'Used by the namespace_loaded_from_symlink test', }; 1; URT.pm100664023532023421 36612544604516 13075 0ustar00abrummetgsc000000000000UR-0.44/tuse strict; use warnings; package URT; use UR; class URT { is => ['UR::Namespace'], has_constant => [ allow_sloppy_primitives => { value => 1 }, ], doc => 'A dummy namespace used by the UR test suite.', }; 1; #$Header 34Baseclass.pm100664023532023421 42412544604516 15137 0ustar00abrummetgsc000000000000UR-0.44/t/URT package URT::34Baseclass; use strict; use warnings; use URT; class URT::34Baseclass { is_transactional => 0, has => [ parent => { is => 'URT::34Subclass', id_by => 'parent_id' }, thingy => { is => 'URT::Thingy', id_by => 'thingy_id' } ] }; 1; 34Subclass.pm100664023532023421 64112544604516 15017 0ustar00abrummetgsc000000000000UR-0.44/t/URT package URT::34Subclass; use strict; use warnings; ## dont "use URT::34Baseclass"; use URT; class URT::34Subclass { isa => 'URT::34Baseclass', is_transactional => 0, has => [ some_other_stuff => { is => 'SCALAR' }, abcdefg => { } ] }; sub create { my $class = shift; my $self = $class->SUPER::create( thingy => URT::Thingy->create ); return $self; } 1; 38Primary.pm100664023532023421 102212544604516 14701 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::38Primary; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::38Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, rel_id => { is => 'Integer'}, related_object => { is => 'URT::38Related', id_by => 'rel_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], data_source => 'URT::DataSource::SomeSQLite1', table_name => 'primary_table', ); 1; 38Related.pm100664023532023421 101512544604516 14640 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::38Related; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::38Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::38Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { via => 'primary_objects', to => 'primary_value', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite2', table_name => 'related', ); 1; 43Primary.pm100664023532023421 67412544604516 14671 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::43Primary; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::43Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, rel_id => { is => 'Integer'}, related_object => { is => 'URT::43Related', id_by => 'rel_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], ); 1; 43Related.pm100664023532023421 67512544604516 14627 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::43Related; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::43Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::43Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { via => 'primary_objects', to => 'primary_value', is_many => 1}, ], ); 1; Testing.pm100664023532023421 43412544604516 16132 0ustar00abrummetgsc000000000000UR-0.44/t/URT/Contextpackage URT::Context::Testing; use strict; use warnings; use UR::Object::Type; use URT; class URT::Context::Testing { is => ['UR::Context::Root'], doc => 'Used by the automated test suite.', }; sub get_default_data_source { "GSC::DataSource::SomeSQLite" } 1; #$Header CircFk.pm100664023532023421 45112544604516 16263 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource package URT::DataSource::CircFk; use strict; use warnings; use UR::Object::Type; use URT; class URT::DataSource::CircFk { is => ['UR::DataSource::SQLite'], }; our $FILE = "/tmp/ur_testsuite_db_$$.sqlite"; IO::File->new($FILE, 'w')->close(); END { unlink $FILE } sub server { $FILE } 1; Meta.pm100664023532023421 214712544604516 16034 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSourcepackage URT::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use UR; UR::Object::Type->define( class_name => 'URT::DataSource::Meta', is => ['UR::DataSource::Meta'], ); use File::Temp; # Override server() so we can make the metaDB file in # a temp dir sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . "_ur_testsuite_metadb" . $self->_extension_for_db; return $PATH; } # Don't print out warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::SomeSQLite sub _dont_emit_initializing_messages { my($dsobj, $message) = @_; if ($message =~ m/^Re-creating/) { # don't emit the message about re-creating the DB when run in the test harness $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { our $PATH; unlink $PATH if ($PATH); } 1; Meta.sqlite3-dump100664023532023421 425512544604516 17751 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSourcePRAGMA foreign_keys = OFF; BEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; Meta.sqlite3-schema100664023532023421 422212544604516 20236 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; SomeFile.pm100664023532023421 64612544604516 16633 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSourcepackage URT::DataSource::SomeFile; use strict; use warnings; use URT; use File::Temp qw(); our(undef, $FILE) = File::Temp::tempfile(); END { unlink $FILE }; class URT::DataSource::SomeFile { is => ['UR::Singleton', 'UR::DataSource::File'], }; sub server { $FILE } sub column_order { return [ qw( thing_id thing_name thing_color ) ]; } sub sort_order { return ['thing_id']; } sub delimiter { "\t" } 1; SomeFileMux.pm100664023532023421 142712544604516 17343 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource package URT::DataSource::SomeFileMux; use strict; use warnings; use UR::Object::Type; use URT; use File::Temp qw(); class URT::DataSource::SomeFileMux { is => ['UR::DataSource::FileMux', 'UR::Singleton'], }; sub constant_values { [ 'thing_type' ] } sub required_for_get { [ 'thing_type' ] } sub column_order { return [ qw( thing_id thing_name thing_color )]; } sub sort_order { return ['thing_id' ] ; } sub delimiter { "\t" } BEGIN { our $BASE_PATH = File::Temp::tempdir( CLEANUP => 1 ); } # Note that the file resolver is called as a normal function (with the parameters # mentioned in requiret_for_get), not as a method with the data source as the # first arg... sub file_resolver { my $type = shift; our $BASE_PATH; return "$BASE_PATH/$type"; } 1; SomeMySQL.pm100664023532023421 116712544604516 16740 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomeMySQL; use URT; class URT::DataSource::SomeMySQL { is => ['UR::DataSource::MySQL'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { 'dbname=somemysql;host='; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { undef; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; SomeOracle.pm100664023532023421 114112544604516 17170 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomeOracle; use URT; class URT::DataSource::SomeOracle { is => ['UR::DataSource::Oracle'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { ''; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { ''; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; SomePostgreSQL.pm100664023532023421 120612544604516 17770 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomePostgreSQL; use URT; class URT::DataSource::SomePostgreSQL { is => ['UR::DataSource::Pg'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { 'dbname=somepostgresql;host='; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { 'public'; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; SomeSQLite.pm100664023532023421 320012544604516 17122 0ustar00abrummetgsc000000000000UR-0.44/t/URT/DataSource package URT::DataSource::SomeSQLite; use strict; use warnings; use File::Temp; BEGIN { my $fh = File::Temp->new(TEMPLATE => 'ur_testsuite_db_XXXX', UNLINK => 0, SUFFIX => '.sqlite3', OPEN => 0, TMPDIR => 1); our $FILE = $fh->filename(); $fh->close(); # The DB file now exists with 0 size our $DUMP_FILE = File::Temp::tmpnam(); } use UR::Object::Type; use URT; class URT::DataSource::SomeSQLite { is => ['UR::DataSource::SQLite','UR::Singleton'], has_optional_constant => [ owner => { value => undef }, ], }; # "delegate" id to UR::Singleton since Perl 5.8 still uses Depth First Search # for Method Resolution Order. sub id { return UR::Singleton::id(@_); } # Don't print warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::Meta. sub _dont_emit_initializing_messages { my($dsobj, $msg) = @_; if ($msg =~ m/^Re-creating|Skipped unload/) { $_[1] = undef; # don't print the message } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { my @paths_to_remove = map { __PACKAGE__->$_ } qw(server _data_dump_path _schema_path); unlink(@paths_to_remove); } # Standard behavior is to put the DB file right next to the module # We'll change that to point to the temp file sub server { our $FILE; return $FILE; } sub _data_dump_path { our $DUMP_FILE; return $DUMP_FILE; } 1; FakeDBI.pm100664023532023421 246512544604516 14304 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::FakeDBI; use strict; use warnings; # A DBI-like test class we can force failures on my %configuration; sub new { my $class = shift; return bless {}, $class; } sub connect { my $class = shift; if ($configuration{connect_fail}) { $class->set_errstr('connect_fail'); return undef; } else { return $class->new(); } } sub configure { my $self = shift; my($key, $val) = @_; $configuration{$key} = $val; } sub prepare { my $self = shift; if ($configuration{prepare_fail}) { $self->set_errstr('prepare_fail'); return undef; } else { return URT::FakeDBI::sth->new($self); } } sub do { my $self = shift; if ($configuration{do_fail}) { $self->set_errstr('do_fail'); return undef; } else { return 1; } } sub set_errstr { my $self = shift; my $key = shift; our $errstr = $configuration{$key}; } sub errstr { our $errstr; return $errstr; } package URT::FakeDBI::sth; sub new { my $class = shift; my $dbh = shift; return bless \$dbh, $class; } sub execute { my $self = shift; my $dbh = $$self; if ($configuration{execute_fail}) { $dbh->set_errstr('execute_fail'); return undef; } else { return 1; } } 1; ObjWithHash.pm100664023532023421 27612544604516 15247 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::ObjWithHash; use warnings; use strict; use URT; class URT::ObjWithHash { has => [ myhash1 => { is => 'HASH' }, mylist => { is => 'ARRAY' }, ], }; 1; RAMThingy.pm100664023532023421 16312544604516 14672 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::RAMThingy; use warnings; use strict; use UR::Object::Type; use URT; class URT::RAMThingy { }; 1; Thingy.pm100664023532023421 174712544604516 14363 0ustar00abrummetgsc000000000000UR-0.44/t/URTpackage URT::Thingy; use warnings; use strict; use UR::Object::Type; use URT; class URT::Thingy { id_by => [ pcr_id => { is => 'NUMBER', len => 15 }, ], has => [ enz_id => { is => 'NUMBER', len => 10, doc => "Link to polymerase used in PCR." }, pcr_name => { is => 'VARCHAR2', len => 64, doc => "GSC name of the pcr_product. Named based on documented naming conventions." }, pri_id_1 => { is => 'NUMBER', len => 10, doc => "Link to one primer used in PCR." }, pri_id_2 => { is => 'NUMBER', len => 10, doc => "Link to one primer used in PCR." }, ], unique_constraints => [ { properties => [qw/pcr_name/], sql => 'PCR_NAME_U' }, ], doc => 'Stores information for each instance of a polymerase chain react', }; 1; Vocabulary.pm100664023532023421 46512544604516 15204 0ustar00abrummetgsc000000000000UR-0.44/t/URT package URT::Vocabulary; use strict; use warnings; use UR::Object::Type; use URT; class URT::Vocabulary { is => ['UR::Vocabulary'], doc => 'A set of words for a given namespace.', }; my @words_with_special_case = (qw//); sub _words_with_special_case { return @words_with_special_case; } 1; 001_util_array_ref_iterator.t100664023532023421 273612544604516 20512 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use above 'UR'; use_ok("UR::Util::ArrayRefIterator"); # Test an array my @a0 = (1, 2, 3, 4, 5); my $i0 = UR::Util::ArrayRefIterator->create(\@a0); for my $v (@a0) { is($i0->next(), $v, sprintf('a0 value %s ok', $v)); } is($i0->next(), undef, 'i0 last value is undef'); is_deeply(\@a0, [1, 2, 3, 4, 5], 'a0 not modified'); # Test an array ref my $a1 = [6, 7, 8, 9]; my $i1 = UR::Util::ArrayRefIterator->create($a1); for my $v (@{$a1}) { is($i1->next(), $v, sprintf('a1 value %s is ok', $v)); } is($i1->next(), undef, 'i1 last value is undef'); is_deeply($a1, [6, 7, 8, 9], 'a1 not modified'); # Make sure we can start at an arbitrary position my $a2 = [10, 11, 12, 13, 14]; my $i2 = UR::Util::ArrayRefIterator->create(arrayref => $a2, position => 2); for my $v (@{$a2}[2..4]) { is($i2->next(), $v, sprintf('a2 value %s is ok', $v)); } is($i2->next(), undef, 'i2 last value is undef'); is_deeply($a2, [10, 11, 12, 13, 14], 'a2 not modified'); # Make sure we handle position > array length my $a3 = [15, 16]; my $i3 = UR::Util::ArrayRefIterator->create(arrayref => $a3, position => 3); is($i3->next(), undef, 'i3 - position > array length is ok'); is_deeply($a3, [15, 16], 'a3 not modified'); # Ensure empty arrays are fine too my $a4 = []; my $i4 = UR::Util::ArrayRefIterator->create($a4); is($i4->next(), undef, 'i4 - empty array->next() is undef'); is_deeply($a4, [], 'a4 not modified'); done_testing(); 001_util_looks_like_class_name.pl100664023532023421 114412544604516 21307 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 10; use above 'UR'; use_ok("UR::Util"); my @tests = ( [ 'Foo' => 1 ], [ 'Foo::Bar' => 1 ], [ 'foo' => 1 ], [ 'foo::bar' => 1 ], [ 'Foo.pm' => '' ], [ 'some/path::name' => '' ], [ 'An::ugly.pl' => '' ], [ '' => '' ], [ '::' => '' ], ); foreach my $test ( @tests ) { my($string, $expected) = @$test; my $got = UR::Util::looks_like_class_name($string); is($got, $expected, $string); } 001_util_on_destroy.t100664023532023421 203712544604516 17006 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use File::Basename; use Test::More; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; sub dummy { eval{} }; plan tests => 7; use UR; my $x = 1; my $sentry = UR::Util::on_destroy { $x = 2; dummy() }; is($x, 1, "value is not updated when the sentry has not been destroyed"); $sentry = undef; is($x, 2, "value is updated when the sentry has been destroyed"); $x = 1; sub foo { my $sentry = UR::Util::on_destroy { $x = 3; dummy(); }; is($x, 1, "value is not updated while the sentry is still in scope"); } foo(); is($x, 3, "value is updated after the sentry goes out of scope"); $x = 1; sub bar { my $sentry = UR::Util::on_destroy { $x = 4; dummy(); }; is($x, 1, "value is updated while the sentry is still in scope"); die "ouch"; } eval { bar(); }; my $exception = $@; is($x, 4, "value is updated after the sentry goes out of scope during thrown exception"); ok($@, "exception is passed through even thogh the sentry does an eval internally: $@"); 00_load.t100664023532023421 56112544604516 14402 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; $ENV{'CALL_COUNT_OUTFILE'} = '/dev/null'; # so Devel::Callcount won't drop a file in the tree plan tests => 2; use_ok( 'UR' ); use_ok( 'UR::All' ); note( "Testing UR $UR::VERSION, Perl $], $^X" ); 01_object.t100664023532023421 250212544604516 14747 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ qw( prop1 prop2 prop3 )], ); my $o = URT::Thing->create(id => 111); ok($o, "made an object"); ok(scalar($o->__changes__), 'Newly created object has changes'); $o = URT::Thing->__define__(id => 222); ok($o, 'defined an object'); ok(! scalar($o->__changes__), 'Newly defined object has no changes'); ok($o->prop1(1), 'Change prop1'); ok(scalar($o->__changes__), 'Object now has changes'); ok(scalar($o->__changes__('prop1')), 'Change to prop1'); ok(! scalar($o->__changes__('prop2')), 'No change to prop2'); $o = URT::Thing->__define__(id => 333, prop1 => 1, prop2 => 2, prop3 => 3); ok($o, 'Define another object with initial values'); ok($o->prop1(99) && $o->prop3(99), 'Change prop1 and prop3'); ok(scalar($o->__changes__), 'Object has changes'); ok(scalar($o->__changes__('prop2','prop3')), 'Object has changes to either prop2 or prop3'); ok(scalar($o->__changes__('prop3')), 'Object has changes to prop3'); ok(! scalar($o->__changes__('id','prop2')), 'Object has no changes to id or prop2'); 1; 02_class_construction.t100664023532023421 2505612544604516 17452 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 35; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $c1 = UR::Object::Type->define(class_name => 'URT::Foo', data_source => "URT::DataSource::SomeSQLite", table_name => "FOO"); is($URT::Foo::ISA[0], 'UR::Entity', "defined class has correct inheritance"); is($URT::Foo::Type::ISA[0], 'UR::Entity::Type', "defined class' meta class has correct inheritance"); my $c1b = UR::Object::Type->get(data_source_id => "URT::DataSource::SomeSQLite", table_name => "FOO"); is($c1b,$c1, "defined class is gettable"); my $c2 = UR::Object::Type->create(class_name => 'URT::Bar', data_source => "URT::DataSource::SomeSQLite", table_name => "BAR"); is($URT::Bar::ISA[0], 'UR::Entity', "created class has correct inheritance"); is($URT::Bar::Type::ISA[0], 'UR::Entity::Type', "created class' meta class has correct inheritance"); my $c2b = UR::Object::Type->get(data_source_id => "URT::DataSource::SomeSQLite", table_name => "BAR"); is($c2b,$c2, "created class is gettable"); my $c3_parent = UR::Object::Type->define( class_name => 'URT::BazParent', id_by => ['id_prop_a','id_prop_b'], has => [ id_prop_a => { is => 'Integer' }, id_prop_b => { is => 'String' }, prop_c => { is => 'Number' }, ], ); ok($c3_parent, 'Created a parent class'); is($URT::BazParent::ISA[0], 'UR::Object', 'defined class has correct inheritance'); is($URT::BazParent::Type::ISA[0], 'UR::Object::Type', "defined class' meta class has correct inheritance"); my %props = map { $_->property_name => $_ } $c3_parent->properties; is(scalar(keys %props), 4, 'Parent class property count correct'); is($props{'id_prop_a'}->is_id, '0 but true', 'id_prop_a is an ID property and has the correct rank'); is($props{'id_prop_b'}->is_id, '1', 'id_prop_b is an ID property and has the correct rank'); is($props{'prop_c'}->is_id, undef, 'prop_c is not an ID property'); my %id_props = map { $_->property_name => 1 } $c3_parent->id_properties; is(scalar(keys %id_props), 3, 'Parent class id property count correct'); is_deeply(\%id_props, { id_prop_a => 1, id_prop_b => 1, id => 1 }, 'all ID properties are there'); my $c3 = UR::Object::Type->define( class_name => 'URT::Baz', is => 'URT::BazParent', has => [ prop_d => { is => 'Number' }, ], ); ok($c3, 'Created class with some properties and a parent class'); is($URT::Baz::ISA[0], 'URT::BazParent', 'defined class has correct inheritance'); is($URT::Baz::Type::ISA[0], 'URT::BazParent::Type', "defined class' meta class has correct inheritance"); %props = map { $_->property_name => $_ } $c3->properties; is(scalar(keys %props), 5, 'property count correct'); is($props{'id_prop_a'}->is_id, '0 but true', 'id_prop_a is an ID property and has the correct rank'); is($props{'id_prop_b'}->is_id, '1', 'id_prop_b is an ID property and has the correct rank'); is($props{'prop_c'}->is_id, undef, 'prop_c is not an ID property'); is($props{'prop_d'}->is_id, undef, 'prop_d is not an ID property'); my $other_class = UR::Object::Type->define( class_name => 'URT::OtherClass', id_by => [ id => { is => 'String' }, ], ); my $parent_with_id_prop = UR::Object::Type->define( class_name => 'URT::ParentWithProp', has => [ other_id => { is => 'Integer' }, ], ); my $child_without_id_prop = UR::Object::Type->define( class_name => 'URT::ChildWithoutProp', is => 'URT::ParentWithProp', has => [ other => { is => 'URT::OtherClass', id_by => 'other_id' } ], ); is($child_without_id_prop->property_meta_for_name('other_id')->data_type, 'Integer', 'implied property gets data_type from parent when specified'); # Test that the id_generator value propogates properly # in-memory class UR::Object::Type->define( class_name => 'URT::InMemory', id_by => 'id' ); is(URT::InMemory->__meta__->id_generator, '-urinternal', 'in-memory class gets default id generator'); # usual case, use the data-source's default sequence generator based on the column and # table name. Blank value in the class meta means delegate to the data source UR::Object::Type->define( class_name => 'URT::DS_No_Idgen', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_no_idgen', id_by => 'id' ); is(URT::DS_No_Idgen->__meta__->id_generator, undef, 'parent SQL-stored class has blank id_generator'); UR::Object::Type->define( is => 'URT::DS_No_Idgen', class_name => 'URT::DS_No_Idgen::Child', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_no_idgen_child', id_by => 'id' ); is(URT::DS_No_Idgen::Child->__meta__->id_generator, undef, 'child SQL-stored class has blank id_generator'); # Parent does not specify id_generator, child does UR::Object::Type->define( is => 'URT::DS_No_Idgen', class_name => 'URT::DS_No_Idgen::Child_has_idgen', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_no_idgen_child_has_idgen', id_generator => 'ds_no_idgen_child_has_idgen_seq', id_by => 'id' ); is(URT::DS_No_Idgen::Child_has_idgen->__meta__->id_generator, 'ds_no_idgen_child_has_idgen_seq', 'Child SQL-stored class can override blank id_generator from parent'); # Parent specifies a sequence generator, the child uses the same one by default UR::Object::Type->define( class_name => 'URT::DS_seq_idgen', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_seq_idgen', id_generator => 'id_seq_idgen_seq', id_by => 'id', ); is(URT::DS_seq_idgen->__meta__->id_generator, 'id_seq_idgen_seq', 'parent SQL-stored class has sequence id_generator'); UR::Object::Type->define( is => 'URT::DS_seq_idgen', class_name => 'URT::DS_seq_idgen::Child', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_seq_idgen_child', id_by => 'id', ); is(URT::DS_seq_idgen::Child->__meta__->id_generator, 'id_seq_idgen_seq', "child SQL-stored class has parent's sequence id_generator"); # Parent specifies a sequence generator, child specifies a different one UR::Object::Type->define( is => 'URT::DS_seq_idgen', class_name => 'URT::DS_seq_idgen::Child2', data_source => 'URT::DataSource::SomeSQLite', table_name => 'ds_seq_idgen_child2', id_generator => 'id_seq_idgen_child2_seq', id_by => 'id', ); is(URT::DS_seq_idgen::Child2->__meta__->id_generator, 'id_seq_idgen_child2_seq', 'child class can specify a different sequence generator than parent'); # parent has uuid generator, child is blank and should inherit the parent's value UR::Object::Type->define( class_name => 'URT::Uuid_idgen', data_source => 'URT::DataSource::SomeSQLite', table_name => 'uuid_idgen', id_generator => '-uuid', id_by => 'id', ); is(URT::Uuid_idgen->__meta__->id_generator, '-uuid', 'parent SQL-stored class uses uuid id_generator'); UR::Object::Type->define( is => 'URT::Uuid_idgen', class_name => 'URT::Uuid_idgen::Child', table_name => 'uuid_idgen_child', id_by => 'id', ); is(URT::Uuid_idgen::Child->__meta__->id_generator, '-uuid', 'child SQL-stored class definition has blank is_generator, but inherits parent value uuid'); subtest 'property_for_column()' => sub { plan tests => 26; my $parent_meta = UR::Object::Type->define( class_name => 'URT::PropForColumnParent', id_by => 'parent_id', has => [ foo => { is => 'String' }, bar => { is => 'Number', column_name => 'bar_custom' }, ], table_name => 'parent_table', data_source => 'URT::DataSource::SomeSQLite', ); my $child_meta = UR::Object::Type->define( class_name => 'URT::PropForColumnChild', is => 'URT::PropForColumnParent', id_by => 'child_id', has => [ foo => { is => 'String' }, bar => { is => 'Number', column_name => 'bar' }, baz => { is => 'Number' }, ], table_name => 'child_table', data_source => 'URT::DataSource::SomeSQLite', ); my $do_tests = sub { my($class_meta, @tests) = @_; for (my $i = 0; $i < @tests; $i += 2) { my($column_name, $expected_property_name) = @tests[$i, $i+1]; is($class_meta->property_for_column($column_name), $expected_property_name, $class_meta->class_name . " column $column_name"); } }; my @parent_tests = ( parent_id => 'parent_id', bogus => undef, bar => undef, bar_custom => 'bar', 'parent_table.parent_id' => 'parent_id', 'parent_table.bogus' => undef, 'parent_table.bar' => undef, 'parent_table.bar_custom' => 'bar', 'bogus_table.parent_id' => undef, ); $do_tests->($parent_meta, @parent_tests); my @child_tests = ( parent_id => 'parent_id', child_id => 'child_id', bogus => undef, foo => 'foo', bar => 'bar', bar_custom => 'bar', baz => 'baz', 'parent_table.parent_id' => 'parent_id', 'child_table.parent_id' => undef, 'parent_table.child_id' => undef, 'child_table.child_id' => 'child_id', 'parent_table.bar' => undef, 'child_table.bar' => 'bar', 'parent_table.bar_custom' => 'bar', 'child_table.bar_custom' => undef, 'parent_table.baz' => undef, 'child_table.baz' => 'baz', ); $do_tests->($child_meta, @child_tests); }; subtest 'inline view property_for_column()' => sub { plan tests => 6; my $class_meta = UR::Object::Type->define( class_name => 'URT::ClassWithInlineView', id_by => 'id', has => [ 'prop_a', 'prop_b' ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select id, prop_a, prop_b from class_with_inline_view where id is not null) class_with_inline_view', ); my @tests = ( 'id' => 'id', 'prop_a' => 'prop_a', 'bogus' => undef, 'class_with_inline_view.prop_a' => 'prop_a', 'class_with_inline_view.bogus' => undef, 'bogus_table.prop_a' => undef, ); for (my $i = 0; $i < @tests; $i += 2) { my($column_name, $expected_property_name) = @tests[$i, $i+1]; is($class_meta->property_for_column($column_name), $expected_property_name, "column $column_name"); } }; 03a_rules.t100664023532023421 1602112544604516 15017 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 45; use Data::Dumper; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, nicknames => { is_many => 1, is => "Integer" }, ], }; my $m = URT::FancyItem->__meta__; ok($m, "got metadata for test class"); my @p = $m->id_property_names; is("@p", "name group", "property names are correct"); my $b = URT::Item->create(name => 'Joe', group => 'shirts'); ok($b, 'made a base class object'); my $p = URT::FancyItem->create(name => 'Bob', group => 'shirts', score => 1, foo => 'foo'); ok($p, "made a parent object"); my $c = URT::FancyItem->create(parent => $p, name => 'Fred', group => 'skins', score => 2); ok($c, "made a child object which references it"); my $u = URT::UnrelatedItem->create(name => 'Bob', group => 'shirts'); ok($u, 'made an unrelated item object'); my $bx1 = URT::Item->define_boolexpr(name => ['Bob','Joe']); my @o = URT::Item->get($bx1); is(scalar(@o), 2, "got 2 items with an in-clause"); ## OR ## my $bx2a = URT::Item->define_boolexpr(name => 'Bob'); my $bx2b = URT::Item->define_boolexpr(group => 'skins'); my $bx2t = UR::BoolExpr::Template::Or->get_by_subject_class_name_logic_type_and_logic_detail( $bx2a->subject_class_name, 'Or', $bx2a->logic_detail . '|' . $bx2b->logic_detail, ); my $bx2c = $bx2t->get_rule_for_values('Bob','skins'); ok(defined($bx2c), "got OR rule: $bx2c"); my ($bx3a,$bx3b) = $bx2c->template->get_underlying_rule_templates(); is($bx3a,$bx2a->template, "first expression in composite matches"); is($bx3b,$bx2b->template, "second expression in composite matches"); my $bx3 = URT::Item->define_boolexpr(-or => [[name => 'Bob'], [group => 'skins']]); ok(defined($bx3), "created OR rule in a single expression"); is_deeply( $bx3, $bx2c, "matches the one individually composed"); my %as_two = map { $_->id => $_ } (URT::Item->get($bx2a), URT::Item->get($bx2b)); my %as_one = map { $_->id => $_ } URT::Item->get($bx3); my @as_two = sort keys %as_two; my @as_one = sort keys %as_one; is("@as_one","@as_two", "results using -or match queries done separately"); # COMPLEX #my $r = URT::FancyItem->define_boolexpr(foo => 222, -recurse => [qw/parent_name name parent_group group/], bar => 555); my $r = URT::Item->define_boolexpr(foo => ''); # '' is the same as undef ok($r, "Created a rule to get URT::Items with null 'foo's"); ok($r->specifies_value_for('foo'), 'Rule specifies a falue for foo'); is($r->value_for('foo'), '', "rule's value for property foo is empty string"); ok(! $r->specifies_value_for('name'), 'rule does not specify a value for name'); my @results = URT::Item->get($r); is(scalar(@results), 2, 'Got 2 URT::Items with the rule'); ok(scalar(grep { $_->name eq 'Joe' } @results), 'Joe was returned'); ok(scalar(grep { $_->name eq 'Fred' } @results), 'Fred was returned'); ok(! scalar(grep { $_->name eq 'Bob' } @results), 'Bob was not returned'); $r = URT::FancyItem->define_boolexpr(foo => 222, -recurse => [parent_name => 'name', parent_group => 'group'], bar => 555); ok($r, "got a rule to get objects using -recurse"); is($r->template->value_position_for_property_name('foo'),0, "position is as expected for variable param 1"); is($r->template->value_position_for_property_name('bar'),1, "position is as expected for variable param 2"); is($r->template->value_position_for_property_name('-recurse'),0, "position is as expected for constant param 1"); my $expected = [foo => 222, -recurse => [qw/parent_name name parent_group group/], bar => 555]; is_deeply( [$r->params_list], $expected, "params list for the rule is as expected" ) or print Dumper([$r->params_list],$expected); my $t = $r->template; ok($t, "got a template for the rule"); is($t->value_position_for_property_name('foo'),0, "position is as expected for variable param 1"); is($t->value_position_for_property_name('bar'),1, "position is as expected for variable param 2"); is($t->value_position_for_property_name('-recurse'),0, "position is as expected for constant param 1"); my @names = $t->_property_names; is("@names","foo bar", "rule template knows its property names"); my $r2 = $t->get_rule_for_values(333,666); ok($r2, "got a new rule from the template with different values for the non-constant values"); is_deeply( [$r2->params_list], [foo => 333, -recurse => [qw/parent_name name parent_group group/], bar => 666], "the new rule has the expected structure" ) or print Dumper([$r->params_list]); $r = URT::FancyItem->define_boolexpr(foo => { operator => "between", value => [10,30] }, bar => { operator => "like", value => 'x%y' }); $t = $r->template(); is($t->operator_for('foo'),'between', "operator for param 1 is correct"); is($t->operator_for('bar'),'like', "operator for param 2 is correct"); $r = URT::FancyItem->define_boolexpr(foo => 10, bar => { operator => "like", value => 'x%y' }); $t = $r->template(); is($t->operator_for('foo'),'=', "operator for param 1 is correct"); is($t->operator_for('bar'),'like', "operator for param 2 is correct"); $r = URT::FancyItem->define_boolexpr(foo => { operator => "between", value => [10,30] }, bar => 20); $t = $r->template(); is($t->operator_for('foo'),'between', "operator for param 1 is correct"); is($t->operator_for('bar'),'=', "operator for param 2 is correct"); # Make a rule on the parent class $r = URT::Item->define_boolexpr(name => 'Bob', group => 'shirts', score => '01'); ok($r->evaluate($p), 'Original parent object evaluated though rule'); ok(! $r->evaluate($c), 'Child object with different params evaluated through parent rule returns false'); $r = URT::Item->define_boolexpr(name => 'Fred', group => 'skins'); ok($r->evaluate($c), 'Child object with same params evaluated through parent rule returns true'); # Make a rule on the child class $r = URT::FancyItem->define_boolexpr(name => 'Joe', group => 'shirts'); ok(! $r->evaluate($b), 'Base class object evaluated through rule on child class returns false'); # An item of a different class but with the same params $r = URT::UnrelatedItem->define_boolexpr(name => 'Bob', group => 'shirts'); ok(! $r->evaluate($p), 'Original parent object evaluated false through rule on unrelatd class'); my $j = URT::UnrelatedItem->create(name => 'James', group => 'shirts', nicknames => [12345, 12347, 34, 36, 37]); $r = URT::UnrelatedItem->define_boolexpr(nicknames => [12347, 82]); ok($r->evaluate($j), 'Many-to-many comparison finds the matching nickname'); 03b_rule_constant_values.t100664023532023421 142412544604516 20106 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests=> 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; class URT::Foo { has => [qw/a b c/]}; my @p1a = (-order_by => [qw/b/], -group_by => [qw/b a/]); my $bx1 = URT::Foo->define_boolexpr(@p1a); my @p1b = $bx1->params_list; is(Data::Dumper::Dumper(\@p1a),Data::Dumper::Dumper(\@p1b), "params list is symmetrical for an expression with two constant values"); my $bx2 = $bx1->normalize; my @p2a = (-group_by => [qw/b a/], -order_by => [qw/b/]); my @p2b = $bx2->params_list; is(Data::Dumper::Dumper(\@p2a),Data::Dumper::Dumper(\@p2b), "params list is symmetrical for an expression with two constant values after normalize"); 03b_rule_subsets.t100664023532023421 1010312544604516 16400 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 24; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, ], }; my($r1, $r2); $r1 = URT::FancyItem->define_boolexpr(); ok($r1->is_subset_of($r1), 'boolexpr with no filters is a subset of itself'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r1), 'boolexpr with one filter is a subset of itself'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'Two rules with the same filters are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); ok($r1->is_subset_of($r2), 'Two rules with the same filters are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(group => 'home', name => 'Bob'); ok($r1->is_subset_of($r2), 'Two rules with the same filters in a different order are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters in a different order are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Fred'); ok(! $r1->is_subset_of($r2), 'Rule with different value for same filter name is not a subset'); ok(! $r2->is_subset_of($r1), 'Rule with different value for same filter name is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(group => 'Bob'); ok(! $r1->is_subset_of($r2), 'Rule with different param names and same value is not a subset'); ok(! $r2->is_subset_of($r1), 'Rule with different param names and same value is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(); ok($r1->is_subset_of($r2), 'one filter is a subset of no filters'); ok(! $r2->is_subset_of($r1), 'converse is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'Rule with two filters is subset of rule with one filter'); ok(! $r2->is_subset_of($r1),' Rule with one filter is not a subset of rule with two filters'); $r1 = URT::FancyItem->define_boolexpr(); $r2 = URT::Item->define_boolexpr(); ok($r1->is_subset_of($r2), 'subset by inheritance with no filters'); ok(! $r2->is_subset_of($r1), 'ancestry is not a subset'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'inheritance and one filter is subset'); ok(! $r2->is_subset_of($r1), 'ancestry and one filter is not a subset'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(group => 'home', name => 'Bob'); ok($r1->is_subset_of($r2), 'inheritance and two filters in different order is subset'); ok(! $r2->is_subset_of($r1), 'ancestry and two filters in different order is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::UnrelatedItem->define_boolexpr(name => 'Bob'); ok(! $r1->is_subset_of($r2), 'Rules on unrelated classes with same filters is not a subset'); ok(! $r2->is_subset_of($r1), 'Rules on unrelated classes with same filters is not a subset'); 03c_rule_values.t100664023532023421 753612544604516 16210 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl # Test handling of rules and their values with different kinds # params. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 21; use Data::Dumper; use IO::Handle; class URT::RelatedItem { id_by => 'ritem_id', has => [ ritem_property => { is => 'String' }, ], }; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, score => { is => 'Integer' }, ritem => { is => 'URT::RelatedItem', id_by => 'ritem_id' }, ] }; my($r, @values, $n, $expected,$fh); $r = URT::Item->define_boolexpr(name => ['Bob'], foo => undef, -hints => ['ritem']); ok($r, 'Created boolexpr'); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 2, 'Got back 2 values from rule'); $expected = [['Bob'], undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # foo, name @values = $n->values(); $expected = [undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); $fh = IO::Handle->new(); $r = URT::Item->define_boolexpr(name => ['Bob'], fh => $fh, foo => undef); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [['Bob'], $fh, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # fh, foo, name @values = $n->values(); $expected = [$fh, undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); $r = URT::Item->define_boolexpr(name => ['Bob'], fh => $fh, foo => undef, -hints => ['ritem']); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [['Bob'], $fh, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # -hints, fh, foo, name @values = $n->values(); $expected = [$fh, undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); my @p = (name => [$fh], score => 1, foo => undef, -hints => ['ritem']); $r = URT::Item->define_boolexpr(@p); my @p2 = $r->params_list(); #is("@p","@p2",'params return correctly with hint'); is_deeply(\@p,\@p2, "match deeply"); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [[$fh], 1, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); is($values[0][0], $p[1][0], 'object is preserved within the arrayref of references'); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # foo, name, score @values = $n->values(); $expected = [undef, [$fh], 1]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); # Check that duplicate values in an in-clause are handled correctly my $rule = URT::Item->define_boolexpr(name => ['Bob', 'Bob', 'Rob', 'Rob', 'Joe', 'Foo']); ok($rule, 'rule with duplicate values created'); my $values = $rule->value_for('name'); my @expected = ('Bob', 'Foo', 'Joe','Rob'); is_deeply($values, \@expected, 'duplicates were filtered out correctly'); 03d_rule_construction.t100664023532023421 1744512544604516 17464 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 162; use Data::Dumper; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "Number", is_optional => 1 }, # These are designed to be similar to things stripped out of BoolExpr keys during resolve() is_id_only => { is => 'Boolean' }, some_param_key => { is => 'Text' }, a_unique_string => { is => 'Text' }, clobber__get_serial_number => { is => 'Number'}, the_change_count => { is => 'Number' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { id_by => [ ui_id => { is => 'Integer' }, ], has => [ name => { is => "String" }, group => { is => "String" }, ], }; my $test_obj = URT::Item->create(name => 'blah', group => 'cool', foo => 'foo', bar => 12345); foreach my $class_name ( qw( URT::Item URT::FancyItem ) ) { foreach my $meta_params ( [], [-group_by => ['bar']] ) { my $bx = $class_name->define_boolexpr(@$meta_params); my $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no filters is not is_partial_id'); ok($tmpl->matches_all, 'Rule template with no filters is matches_all'); $bx = $class_name->define_boolexpr(name => 'blah', @$meta_params); $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with one ID property filter is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with one ID property filter is not is_id_only'); ok($tmpl->is_partial_id, 'Rule template with one ID property filter is is_partial_id'); ok(!$tmpl->matches_all, 'Rule template with one ID property filter is not matches_all'); $bx = $class_name->define_boolexpr(name => 'blah', group => 'foo', @$meta_params); $tmpl = $bx->template; ok($bx->is_id_only, 'Rule with both ID property filters is is_id_only'); ok($tmpl->is_id_only, 'Rule template with both ID property filters is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with both ID property filter is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with both ID property filter is not matches_all'); $bx = $class_name->define_boolexpr(parent_name => '12345', @$meta_params); $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no ID filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no ID filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no ID filters is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with no ID filters is not matches_all'); } } foreach my $meta_params ( [], [-group_by => ['group']] ) { my $bx = URT::UnrelatedItem->define_boolexpr(@$meta_params); my $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no filters is not is_partial_id'); ok($tmpl->matches_all, 'Rule template with no filters is matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(ui_id => 1, @$meta_params); $tmpl = $bx->template; ok($tmpl->is_id_only, 'Rule with the single ID param is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule with the single ID param is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule with the single ID param is not matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(ui_id => [2], @$meta_params); $tmpl = $bx->template; ok($tmpl->is_id_only, 'Rule with the single ID in-clause param is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule with the single ID in-clause param is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule with the single ID in-clause param is not matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(name => 'foo', @$meta_params); $tmpl = $bx->template; ok(! $tmpl->is_id_only, 'Rule template with no ID filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no ID filters is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with no ID filters is not matches_all'); } my @tests = ( # get params property operator expected val [ [ name => 'blah'], 'name', '=', 'blah' ], [ [ name => { operator => '=', value => 'blah'}], 'name', '=', 'blah' ], [ [ 'name =' => 'blah'], 'name', '=', 'blah' ], [ [ name => undef], 'name', '=', undef ], [ [ bar => 1 ], 'bar', '=', 1 ], [ [ bar => { operator => '<', value => 1 }], 'bar', '<', 1 ], [ [ name => [ 'bob', 'joe', 'frank' ] ], 'name', 'in', ['bob','frank','joe']], # list values are sorted [ [ name => { operator => 'not in', value => [1,2,3]} ], 'name', 'not in', [1,2,3] ], [ [ 'name in', => [ 'bob', 'joe', 'frank' ] ], 'name', 'in', ['bob','frank','joe']], [ [ 'name not in' => [ 'bob', 'joe', 'frank' ] ], 'name', 'not in', ['bob','frank','joe']], [ [ name => [ undef ] ], 'name', 'in', [undef] ], [ [ name => { operator => 'in', value => [ undef ] } ], 'name', 'in', [undef] ], [ [ 'name in' => [undef] ], 'name', 'in', [undef] ], [ [ 'name in' => [ 1, undef]], 'name', 'in', [1, undef] ], [ [ bar => { operator => 'between', value => [0,3] } ], 'bar', 'between', [0,3] ], [ [ bar => { operator => 'not between', value => [0,3] } ], 'bar', 'not between', [0,3] ], [ [ 'bar between' => [0,3] ], 'bar', 'between', [0,3] ], [ [ 'bar not between' => [0,3] ], 'bar', 'not between', [0,3] ], [ [ parent => $test_obj ], 'parent_name', '=', 'blah' ], [ [ parent => $test_obj ], 'parent_group','=', 'cool' ], [ [ is_id_only => 1 ], 'is_id_only', '=', 1 ], [ [ a_unique_string => 'hithere'], 'a_unique_string', '=', 'hithere' ], [ [ clobber__get_serial_number => 123], 'clobber__get_serial_number','=', 123], [ [ the_change_count => 456], 'the_change_count', '=', 456], ); for( my $i = 0; $i < @tests; $i++) { my $test = $tests[$i]; my @rule_params = @{ $test->[0] }; my $r = URT::Item->define_boolexpr(@rule_params); ok($r, "Defined a BoolExpr for test $i"); my($property, $expected_operator, $expected_value) = @$test[1..3]; my $got_operator = $r->operator_for($property); is($got_operator, $expected_operator, "Operator for $property is '$expected_operator'"); my $got_value = $r->value_for($property); is_deeply($got_value, $expected_value, "Value for $property matched"); } 03e_params_list.t100664023532023421 531412544604516 16172 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 7; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'primary_car_color like' => 'red%', 'primary_car.engine.size' => [428,429], 'cars.color in' => ['red','blue'], ); my $bx2 = URT::Person->define_boolexpr( -or => [ [ 'is_cool' => 1, 'cars.color in' => ['red','blue'], ], [ 'primary_car_color like' => 'red%', 'primary_car.engine.size' => [428,429], ], ] ); for my $bx ($bx1, $bx2) { my @pa = $bx->params_list; my @pb = $bx->_params_list; my $bxa = URT::Person->define_boolexpr(@pa); is($bxa->id, $bx->id, "the params_list reconstructs the same object $bxa"); my $bxb = URT::Person->define_boolexpr(@pb); is($bxb->id, $bx->id, "the params_list reconstructs the same object $bxb"); } 03f_rule_from_filter_string.t100664023532023421 6617512544604516 20636 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl # Test the Parse::YAPP parser used by the Lister commands use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 728; class URT::RelatedItem { id_by => 'ritem_id', has => [ ritem_property => { is => 'String' }, ritem_number => { is => 'Number' }, ], }; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, score => { is => 'Integer' }, ritem => { is => 'URT::RelatedItem', id_by => 'ritem_id' }, desc => { is => 'String' }, ] }; foreach my $test ( { string => q(name~%foo 123%), values => { name => '%foo 123%' }, operators => { name => 'like' }, }, { string => q(name~%foo 123%,score=5), values => { name => '%foo 123%', score => 5 }, operators => { name => 'like', score => '='}, }, { string => 'name = bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name=bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name=>bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name != bob', values => { name => 'bob' }, operators => { name => 'not =' }, }, { string => 'name!=bob', values => { name => 'bob' }, operators => { name => 'not =' }, }, { string => 'name=a-longer-string', values => { name => 'a-longer-string' }, operators => { name => '=' }, }, { string => 'name=2012-jan-12', values => { name => '2012-jan-12' }, operators => { name => '=' }, #stop => 1, }, { string => 'name=some.thing', values => { name => 'some.thing' }, operators => { name => '='}, }, { string => 'name=/some/file.path.ext', values => { name => '/some/file.path.ext' }, operators => { name => '='}, }, { string => 'name=Some::Class::Name', values => { name => 'Some::Class::Name' }, operators => { name => '='}, }, { string => 'name:Some::Class/Other::Class/Third::Class,score =2', values => { name => ['Other::Class','Some::Class','Third::Class'], score => 2 }, operators => { name => 'in', score => '='}, }, { string => 'name in [Some::Class, Other::Class, Third::Class] and score = 2', values => { name => ['Other::Class','Some::Class','Third::Class'], score => 2 }, operators => { name => 'in', score => '='}, }, { string => 'name=fred and score>2', values => { name => 'fred', score => 2 }, operators => { name => '=', score => '>'}, }, { string => 'name=",",score=2', values => { name => ',', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name=and and score=2' , values => { name => 'and', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name in [bob,fred] and score<-2', values => { name => ['bob','fred'], score => -2 }, operators => { name => 'in', score => '<' } }, { string => 'score = -12.2' , values => { score => -12.2 }, operators => { score => '=' }, }, { string => 'score = .2' , values => { score => .2 }, operators => { score => '=' }, }, { string => 'score = -.2' , values => { score => -0.2 }, operators => { score => '=' }, }, { string => 'name=fred and score>2,foo=bar', values => { name => 'fred', score => 2, foo => 'bar' }, operators => { name => '=', score => '>', foo => '='} }, { string => 'name=fred and score>=2', operators => { name => '=', score => '>=' }, values => { name => 'fred', score => 2}, }, { string => 'name=fred and score<=2', operators => { name => '=', score => '<=' }, values => { name => 'fred', score => 2}, }, { string => 'score!:-100--10.2', values => { score => [-100, -10.2] }, operators => { score => 'not between' }, }, { string => 'name~%yoyo,score:10-100', values => { name => '%yoyo', score => [10,100] }, operators => { name => 'like', score => 'between' } }, { string => 'name like yoyo', values => { name => '%yoyo%' }, operators => { name => 'like' } }, { string => 'name like something-with-dashes1795%', values => { name => 'something-with-dashes1795%' }, operators => { name => 'like' }, }, { string => 'name like H_%-MPaS3387-1795-lib2', values => { name => 'H_%-MPaS3387-1795-lib2' }, operators => { name => 'like' }, }, { string => 'name like %some/file/path-name.ext', values => { name => '%some/file/path-name.ext' }, operators => { name => 'like' }, }, { string => 'name like 1234% and desc not like %bar%', values => { name => '1234%', desc => '%bar%' }, operators => { name => 'like', desc => 'not like' }, }, { string => 'foo:one/two/three', values => { foo => ['one','three','two'] }, # They get sorted internally operators => { foo => 'in' }, }, { string => 'foo!:one/two/three', values => { foo => ['one','three','two'] }, # They get sorted internally operators => { foo => 'not in' }, }, { string => 'name=/a/path/name', values => { name => '/a/path/name' }, operators => { name => '=' }, }, { string => 'name:a/path/name', values => { name => ['a','name','path'] }, operators => { name => 'in' }, }, { string => 'name in ["/a/path/name","/other/path/","relative/path/name"]', values => { name => ['/a/path/name','/other/path/','relative/path/name'] }, operators => {name => 'in' }, }, { string => 'score in [1,2,3]', values => { score => [1,2,3] }, operators => { score => 'in' }, }, { string => 'score not in [1,2,3]', values => { score => [1,2,3] }, operators => { score => 'not in' }, }, { string => 'foo:one/two/three,score:10-100', # These both use : values => { foo => ['one','three','two'], score => [10,100] }, operators => { foo => 'in', score => 'between' }, }, { string => 'foo!:one/two/three,score:10-100', # These both use : values => { foo => ['one','three','two'], score => [10,100] }, operators => { foo => 'not in', score => 'between' }, }, { string => q(name="bob is cool",foo:'one "two"'/three), values => { name => 'bob is cool', foo => ['one "two"','three'] }, operators => { name => '=', foo => 'in' }, }, { string => 'name not like %joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name ! like %joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name !~%joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name not like %joe and score!:10-100 and foo!:one/two/three', values => { name => '%joe', score => [10,100], foo => ['one', 'three', 'two'] }, operators => { name => 'not like', score => 'not between', foo => 'not in' } }, { string => 'name=foo and ritem.ritem_property=bar', values => { name => 'foo', 'ritem.ritem_property' => 'bar' }, operators => { name => '=', 'ritem.ritem_property' => '=' }, }, { string => 'name=foo,ritem.ritem_property=bar,ritem.ritem_number=.2', values => { name => 'foo', 'ritem.ritem_property' => 'bar','ritem.ritem_number' => 0.2 }, operators => { name => '=', 'ritem.ritem_property' => '=', 'ritem.ritem_number' => '=' }, }, { string => 'name=foo and foo=bar and score=2', values => { name => 'foo', foo => 'bar', score => 2 }, operators => { name => '=', foo => '=', score => '=' }, }, { string => 'name=foo and ( foo=bar and score=2 )', values => { name => 'foo', foo => 'bar', score => 2 }, operators => { name => '=', foo => '=', score => '=' }, }, { string => 'name=foo limit 10', values => { name => 'foo' }, operators => {name => '='}, limit => 10, }, { string => 'name=foo offset 10', values => { name => 'foo' }, operators => {name => '='}, offset => 10, }, { string => 'name=foo limit 10 offset 20', values => { name => 'foo' }, operators => {name => '='}, limit => 10, offset => 20, }, { string => 'name=foo and score=2 limit 10 offset 20', values => { name => 'foo', score => 2 }, operators => {name => '=', score => '='}, limit => 10, offset => 20, }, { string => 'name=foo order by score' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score'], }, { string => 'name=foo order by score asc' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score'], }, { string => 'name=foo order by -score' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score'], }, { string => 'name=foo order by score desc' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score'], }, { string => 'name=foo order by score,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score asc,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score asc,foo asc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score,-foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','-foo'], }, { string => 'name=foo order by score,foo desc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','-foo'], }, { string => 'name=foo order by -score,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by score desc,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by score desc,foo asc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by -score,-foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], }, { string => 'name=foo order by score desc,foo desc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], }, { string => 'name=foo order by -score,-foo group by ritem_id', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id'], }, { string => 'name=foo order by score desc,foo desc group by ritem_id', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id'], }, { string => 'name=foo order by -score,-foo group by ritem_id, parent_name', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id','parent_name'], }, { string => 'name=foo order by -score,-foo group by ritem_id, parent_name limit 10 offset 20', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id','parent_name'], limit => 10, offset => 20, }, { string => '', values => {}, operators => {}, }, { string => 'order by score', values => {}, operators => {}, order_by => ['score'], }, { string => 'name = a string and score=2', values => { name => 'a string', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name=a string with some more words and score = 2', values => { name => 'a string with some more words', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name=a string with spaces in between the words and score =2', values => { name => 'a string with spaces in between the words', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name=a string with multiple spaces and score = 2', values => { name => 'a string with multiple spaces', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name true', operators => { name => 'true' }, values => { name => 1 }, }, { string => 'name false', operators => { name => 'false' }, values => { name => 1 }, }, { string => 'name true and score=2', operators => { name => 'true', score => '=' }, values => { name => 1, score => 2 }, }, { string => 'name is null', operators => { name => '=' }, values => { name => undef }, }, { string => 'name is not null', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name is undef', operators => { name => '=' }, values => { name => undef }, }, { string => 'name is not undef', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name not is undef', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name not is null', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name is not undef and score=2', operators => { name => '!=', score => '=' }, values => { name => undef, score => 2 }, }, { string => 'name=this that + the other thing', operators => { name => '=' }, values => { name => 'this that + the other thing' }, }, ) { $DB::single = 1 if ($test->{'stop'}); my $string = $test->{'string'}; my $values = $test->{'values'}; my $value_count = scalar(values %$values); my @properties = keys %$values; my $operators = $test->{'operators'}; my $r = UR::BoolExpr->resolve_for_string( 'URT::Item', $test->{'string'}); ok($r, "Created rule from string \"$string\""); my @got_values = $r->values(); is(scalar(@got_values), $value_count, 'Rule has the right number of values'); foreach my $property (@properties) { is_deeply($r->value_for($property), $values->{$property}, "Value for $property is correct"); is($r->operator_for($property), $operators->{$property}, "Operator for $property is correct"); } foreach my $meta ( 'order_by', 'group_by', 'limit', 'offset' ) { if ($test->{$meta}) { my $got = $r->template->$meta; is_deeply($got, $test->{$meta}, "$meta is correct"); } } exit if ($test->{'stop'}); } # or-type rules need to be checked differently foreach my $test ( { string => 'name=bob or foo=bar', rules => [ { values => { name => 'bob' }, operators => { name => '=' }, }, { values => { foo => 'bar' }, operators => { foo => '=' }, } ], }, { string => 'name=bob and score=2 or name =fred and foo=bar', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'fred', foo => 'bar' }, operators => { name => '=', foo => '=' }, } ], }, { string => 'name=bob or name=foo or foo=bar', rules => [ { values => { name => 'bob' }, operators => { name => '=' }, }, { values => { name => 'foo' }, operators => { name => '=' }, }, { values => { foo => 'bar' }, operators => { foo => '=' }, }, ], }, { string => 'name=bob and (score=2 or foo=bar)', rules => [ { values => { name => 'bob', score => 2, }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => 'bar' }, operators => { name => '=', foo => '=' }, }, ], }, { string => '(name=bob or name=joe) and (score = 2 or score = 4)', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', score => 4 }, operators => { name => '=', score => '=' }, }, { values => { name => 'joe', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'joe', score => 4 }, operators => { name => '=', score => '=' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => q( name=bob and (score = 2 or ( foo = bar and (parent_name=joe or ((group=cool or ritem.ritem_number<0.123) and (ritem_id = 123 or ritem.ritem_property=mojo)))))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => 'bar', parent_name => 'joe' }, operators => { name => '=', foo => '=', parent_name => '=' }, }, { values => { name => 'bob', foo => 'bar', group => 'cool', ritem_id => 123 }, operators => { name => '=', foo => '=', group => '=', ritem_id => '=' }, }, { values => { name => 'bob', foo => 'bar', group => 'cool', 'ritem.ritem_property' => 'mojo' }, operators => { name => '=', foo => '=', group => '=', 'ritem.ritem_property' => '=' }, }, { values => { name => 'bob', foo => 'bar', 'ritem.ritem_number' => 0.123, ritem_id => 123 }, operators => { name => '=', foo => '=', 'ritem.ritem_number' => '<', ritem_id => '=' }, }, { values => { name => 'bob', foo => 'bar', 'ritem.ritem_number' => 0.123, 'ritem.ritem_property' => 'mojo' }, operators => { name => '=', foo => '=', 'ritem.ritem_number' => '<', 'ritem.ritem_property' => '=' }, }, ], }, ) { $DB::single = 1 if ($test->{'stop'}); my $string = $test->{'string'}; my $composite_rule = UR::BoolExpr->resolve_for_string('URT::Item',$string); ok($composite_rule, "Created rule from string \"$string\""); isa_ok($composite_rule->template, 'UR::BoolExpr::Template::Or'); my @r = $composite_rule->underlying_rules(); is(scalar(@r), scalar(@{$test->{'rules'}}), 'Underlying rules count is correct'); for (my $i = 0; $i< @{ $test->{'rules'}}; $i++) { my $r = $r[$i]; my $test_rule = $test->{'rules'}->[$i]; my $values = $test_rule->{'values'}; my $value_count = $test_rule->{'override_value_count'} || scalar(values %$values); my @properties = keys %$values; my $operators = $test_rule->{'operators'}; my @got_values = $r->values(); is(scalar(@got_values), $value_count, "Composite rule $i has the right number of values"); foreach my $property (@properties) { is_deeply($r->value_for($property), $values->{$property}, "Value for $property is correct"); is($r->operator_for($property), $operators->{$property}, "Operator for $property is correct"); } } exit if ($test->{'stop'}); } foreach my $test ( { string => 'name in bob/fred and score<-2', exception => qr{Syntax error near token WORD 'bob/fred'.*Expected one of: LEFT_BRACKET}s, }, { string => 'name:[bob,fred] and score<-2', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'name:/a/path/name', exception => qr{Syntax error near token IN_DIVIDER '/'}, }, { string => 'score=[1,2,3]', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'score!=[1,2,3]', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'name=foo order by -score desc', exception => qr{Syntax error near token DESC_WORD 'desc'}, }, { string => 'name=foo order by -score asc', exception => qr{Syntax error near token ASC_WORD 'asc'}, }, { string => 'name=foo order by score desc asc', exception => qr{Syntax error near token ASC_WORD 'asc'}, }, ) { my $string = $test->{'string'}; my $exception_re = $test->{'exception'}; my $r; eval { $r = UR::BoolExpr->resolve_for_string( 'URT::Item', $test->{'string'}); }; ok(!$r, "Correctly did not create rule from string \"$string\""); like($@, $exception_re, 'exception looks right'); } 1; 03g_rule_constant_key_before.t100664023532023421 474412544604516 20736 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; ok(setup_classes_and_db(), 'setup classes and DB') or die; # 'constant key before' tests were added due to bug that occurred when constant keys were specified # before expanded properties. Since values are split based on whether they are constant (go on template) # or non-constant (go on rule) there was a mismatch when a rule was normalized. do { # This would work BUT was not equivalent to switching the -order and id k/v pairs. my @phone = UR::Context->current->reload('Phone', id => [0], -order => []); is(scalar @phone, 1, 'constant key after expanded property (op: in)'); }; do { # This would work since make is not expanded. my @phone = UR::Context->current->reload('Phone', -order => [], make => ['Nokia']); is(scalar @phone, 1, 'constant key before non-expanded property'); }; do { my @phone = UR::Context->current->reload('Phone', -order => [], id => [0]); is(scalar @phone, 1, 'constant key before expanded property (op: in)'); }; do { my @phone = UR::Context->current->reload('Phone', -order => [], id => 0); is(scalar @phone, 1, 'constant key before expanded property (op: eq)'); }; done_testing(); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table phones (phone_id integer, make varchar, model varchar)'), 'created phones table'); my @phone_specs = ( ['Motorola', 'Atrix'], ['Motorola', 'Droid Razr'], ['Nokia', 'N9'], ); my $insert = $dbh->prepare('insert into phones (phone_id, make, model) values (?,?,?)'); for (my $id = 0; $id < @phone_specs; $id++) { unless ($insert->execute($id, @{$phone_specs[$id]})) { die "Couldn't insert a row into 'phones': $DBI::errstr"; } } $dbh->commit; my $phone_type = UR::Object::Type->define( class_name => 'Phone', id_by => [ phone_id => { is => 'Number' }, ], has => [ make => { is => 'Text' }, model => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'phones', ); isa_ok($phone_type, 'UR::Object::Type', 'defined Phone class'); is(Phone->class, 'Phone', 'Phone class is loaded'); return 1; } 03h_rule_for_property_meta.t100664023532023421 136612544604516 20451 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; class My::Foo { attributes_have => [ is_blah => { is => 'Boolean' }, ], has => [ foo => { is => 'Text', is_blah => 1 }, bar => { is => 'Text', is_blah => 0 }, ] }; my $meta = My::Foo->__meta__; my @p; @p = $meta->properties(is_blah => 1); is(scalar(@p), 1, "got just one property"); is($p[0]->property_name, "foo", "got the expected property"); @p = $meta->properties(is_blah => 0); is(scalar(@p), 1, "got just one property"); is($p[0]->property_name, "bar", "got the expected property"); 03i_non_ur_types_as_values.t100664023532023421 526412544604516 20452 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 55; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; use IO::Handle; use IO::File; # A non-UR Perl class package OtherScalar; our @ISA = (); package SomeScalar; our @ISA = qw(OtherScalar); package main; ok(UR::Object::Type->define( class_name => 'URT::Person', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, list_thing => { is => 'ARRAY' }, glob_thing => { is => 'GLOB' }, handle_thing => { is => 'IO::Handle' }, scalar_thing => { is => 'SCALAR' }, code_thing => { is => 'CODE' }, hash_thing => { is => 'HASH' }, ref_thing => { is => 'REF' }, ], ), "created class for Person"); my $listref = [1,2,3]; my $blessed_listref = [1,2,3]; bless $blessed_listref,'ListRef'; my $handle = IO::Handle->new(); my $filehandle = IO::File->new(); our $FOO; my $globref = \*FOO; my $scalarref = \"hello"; my $blessed_scalarref; { my $string = "hello"; $blessed_scalarref = \$string; bless $blessed_scalarref, 'ScalarRef'; } my $other_scalarref; # In package SomeScalar, which ISA OtherScalar { my $string = "hello"; $other_scalarref = \$string; bless $other_scalarref, 'SomeScalar'; } my $coderef = sub {1;}; my $blessed_coderef = sub {1;}; bless $blessed_coderef, 'CodeRef'; my $hashref = { one => 1, two => 2 }; my $blessed_hashref = { one => 1, two => 2 }; bless $blessed_hashref, 'HashRef'; my $refref = \$listref; my $blessed_refref = \$listref; bless $blessed_refref, 'RefRef'; my @tests = ( [ name => 'Bob' ], [ list_thing => $listref ], [ glob_thing => $handle ], [ glob_thing => $filehandle ], [ glob_thing => $globref ], [ handle_thing => $handle ], [ handle_thing => $filehandle ], [ scalar_thing => $scalarref ], [ scalar_thing => $blessed_scalarref ], [ scalar_thing => $other_scalarref ], [ scalar_thing => 1 ], [ code_thing => $coderef ], [ code_thing => $blessed_coderef ], [ hash_thing => $hashref ], [ hash_thing => $blessed_hashref ], [ ref_thing => $refref ], [ ref_thing => $blessed_refref ], [ ref_thing => $hashref ], ); foreach my $test ( @tests ) { my($bx,%extra) = URT::Person->define_boolexpr( @$test ); ok($bx, 'Created BoolExpr with params '.join(',',@$test)); is($bx->value_for($test->[0]), $test->[1], 'Value for param is correct'); is(scalar(keys %extra), 0, 'No params were rejected by define_boolexpr()'); } 03i_rule_hard_refs.t100664023532023421 453712544604516 16652 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl # Test handling of rules and their values with different kinds # params. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 9; use Data::Dumper; use IO::Handle; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, scores => { is => 'ARRAY' }, things => { is => 'HASH' }, relateds => { is => 'URT::Related', reverse_as => 'item', is_many => 1 }, related_ids => { via => 'relateds', to => 'id', is_many => 1 }, ] }; class URT::Related { has => { item => { is => 'URT::Item', id_by => 'item_id' }, } }; my $scores = [1,2,3]; my $things = {'one' => 1, 'two' => 2, 'three' => 3}; my $related_ids = [1,2,3]; my $rule = URT::Item->define_boolexpr(name => 'Bob', scores => $scores, things => $things, related_ids => $related_ids); ok($rule, 'Created boolexpr'); is($rule->value_for('name'), 'Bob', 'Value for name is correct'); is($rule->value_for('scores'), $scores, 'Getting the value for "scores" returns the exact same array as was put in'); is($rule->value_for('things'), $things, 'Getting the value for "things" returns the exact same hash as was put in'); is($rule->value_for('related_ids'), $related_ids, 'Getting the value for "related_ids" does not return the exact same array as was put in'); my $tmpl = UR::BoolExpr::Template->resolve('URT::Item', 'name','scores','things','related_ids'); ok($tmpl, 'Created BoolExpr template'); my $rule_from_tmpl = $tmpl->get_rule_for_values('Bob', $scores, $things,$related_ids); #ok($rule_from_tmpl, 'Created BoolExpr from that template'); TODO: { local $TODO = "rules created from get_rule_for_values() don't have their hard refs properly saved"; is($rule_from_tmpl->value_for('scores'), $scores, 'Getting the value for "scores" returns the exact same array as was put in'); is($rule_from_tmpl->value_for('things'), $things, 'Getting the value for "things" returns the exact same hash as was put in'); is($rule_from_tmpl->value_for('related_ids'), $related_ids, 'Getting the value for "related_ids" does not return the exact same array as was put in'); } 03i_rule_hard_refs_with_ur_objects.t100664023532023421 1512612544604516 22140 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 71; # Test the case where UR objects get serialized in a BoolExpr's value # as FreezeThaw data. When the objects come back out, the objects need # to be from the object cache, and not cloned versions of them use Scalar::Util qw(refaddr); class URT::Item { has => [ scalar => { is => 'SCALAR' }, array => { is => 'ARRAY' }, hash => { is => 'HASH' }, linked_list => { is => 'LinkedListNode' }, reference => { is => 'REF' }, ] }; class URT::ListElement { has => { name => { is => 'String' }, } }; my @ELEMENT_NAMES = qw(foo bar baz foo); # foo is in there twice is(scalar(create_elements()), scalar(@ELEMENT_NAMES), 'create list elements'); test_arrayref(); test_hashref(); test_self_referential_data(); test_refref(); test_mixed_arrayref(); sub create_elements { map { URT::ListElement->get_or_create(name => $_) } @ELEMENT_NAMES; } sub test_arrayref { my @elements = map { URT::ListElement->get(name => $_) } @ELEMENT_NAMES; my $bx_id; { my $bx = URT::Item->define_boolexpr(array => \@elements); ok($bx, 'Create boolexpr comtaining arrayref of UR objects'); my $got_elements = $bx->value_for('array'); elements_match($got_elements, \@elements); $bx_id = $bx->id; } # Original bx goes out of scope { my $bx = UR::BoolExpr->get($bx_id); ok($bx, 'Retrieve BoolExpr with arrayref by id'); my $got_elements = $bx->value_for('array'); elements_match($got_elements, \@elements); } } sub test_hashref { my @elements = map { URT::ListElement->get(name => $_) } @ELEMENT_NAMES; my $bx_id; { # Besides testing a hashref, also test that it will recurse into # nested data structures my %h = map { $_->name => [ { $_->name => $_ } ] } @elements; my $bx = URT::Item->define_boolexpr(hash => \%h); ok($bx, 'Create boolexpr containing hashref of UR Objects'); my $got_elements = $bx->value_for('hash'); is(ref($got_elements), 'HASH', 'Got back hashref'); elements_match( _extract_UR_objects_from_test_hashref($got_elements), \@elements, ); $bx_id = $bx->id; } # original bx goes out of scope { my $bx = UR::BoolExpr->get($bx_id); ok($bx, 'Retrieve BoolExpr with hashref by id'); my $got_elements = $bx->value_for('hash'); elements_match( _extract_UR_objects_from_test_hashref($got_elements), \@elements, ); } } sub test_self_referential_data { my @elements = URT::ListElement->get(name => \@ELEMENT_NAMES); # make a linked list my $linked_list; my $last; foreach my $element ( @elements ) { my $node = { element => $element, next => undef, }; if ($linked_list) { $last->{next} = $node; } else { $linked_list = $node; } $last = $node; } # make the linked list circular $last->{next} = $linked_list; # Flag an error and exit if unfreezing the rule data goes into deep recursion local $SIG{__WARN__} = sub { ok(0, 'deep recursion'); die; }; my $bx_id; { my $bx = URT::Item->define_boolexpr(linked_list => $linked_list); ok($bx, 'Create boolexpr containing linked_list with UR Objects'); my $got_list = $bx->value_for('linked_list'); is(ref($got_list), 'HASH', 'Got back linked list head'); elements_match( [_extract_UR_objects_from_test_linked_list($got_list)], \@elements, ); $bx_id = $bx->id; } # original bx goes out of scope { my $bx = UR::BoolExpr->get($bx_id); ok($bx, 'Retrieve BoolExpr with linked_list by id'); my $got_list = $bx->value_for('linked_list'); elements_match( [ _extract_UR_objects_from_test_linked_list($got_list) ], \@elements, ); } } sub test_refref { my @elements = map { URT::ListElement->get(name => $_) } @ELEMENT_NAMES; my $elements_ref = \@elements; my $bx_id; { my $bx = URT::Item->define_boolexpr(reference => \$elements_ref); ok($bx, 'Create boolexpr comtaining ref to arrayref of UR objects'); my $got_elements = $bx->value_for('reference'); elements_match($$got_elements, $elements_ref); $bx_id = $bx->id; } # Original bx goes out of scope { my $bx = UR::BoolExpr->get($bx_id); ok($bx, 'Retrieve BoolExpr with arrayref by id'); my $got_elements = $bx->value_for('reference'); elements_match($$got_elements, $elements_ref); } } sub test_mixed_arrayref { local $SIG{__WARN__} = sub { ok(0, 'unexpected warning: '.shift); die; }; my @elements = ( 1, 2, undef, URT::ListElement->get(name => \@ELEMENT_NAMES), undef, 2, 0); my $bx_id; { my $bx = URT::Item->define_boolexpr(array => \@elements); ok($bx, 'Create boolexpr comtaining arrayref of mixed UR objects and non-ref data'); my $got_elements = $bx->value_for('array'); elements_match($got_elements, \@elements); $bx_id = $bx->id; } # Original bx goes out of scope { my $bx = UR::BoolExpr->get($bx_id); ok($bx, 'Retrieve BoolExpr with arrayref by id'); my $got_elements = $bx->value_for('array'); elements_match($got_elements, \@elements); } } sub _extract_UR_objects_from_test_hashref { my $data = shift; my @elements; foreach my $name ( @ELEMENT_NAMES ) { push @elements, $data->{$name}->[0]->{$name}; } return \@elements; } sub _extract_UR_objects_from_test_linked_list { my $list = shift; my %seen; my $visit; $visit = sub { my $node = shift; return $seen{$node}++ ? () : ( $node->{element}, $visit->($node->{next})); }; return $visit->($list); } sub elements_match { my($got_elements, $elements) = @_; is(scalar(@$got_elements), scalar(@$elements), 'Number of elements match'); for (my $i = 0; $i < @$got_elements; $i++) { if (ref($got_elements->[$i]) and ref($elements->[$i])) { is(refaddr($got_elements->[$i]), refaddr($elements->[$i]), "Element $i is the same reference"); } else { is($got_elements->[$i], $elements->[$i], "Element $i matches"); } } } 03j_or_rules_with_meta.t100664023532023421 1063512544604516 17576 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 32; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, parant_name => { is => 'String', via => 'parent', to => 'name' }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, nicknames => { is_many => 1, is => "Integer" }, ], }; # First an easy one.... my $bx = URT::FancyItem->define_boolexpr(name => 'Fred', -order => [ 'bar' ]); ok($bx, 'Made a simple rule with -order'); ok($bx->specifies_value_for('name'), 'Rule has value for name'); is($bx->value_for('name'), 'Fred', 'Rule has correct value for for name'); ok(! $bx->specifies_value_for('foo'), 'Rule correctly has no value for foo'); is_deeply($bx->value_for('-order'), ['bar'], 'Rule has correct value for -order'); # Try a compound rule $bx = URT::FancyItem->define_boolexpr(-or => [ [ name => 'Fred' ], [foo => 'bar'] ], -order => [ 'bar' ]); ok($bx, 'Make Or-type rule with -order'); my @underlying = $bx->underlying_rules(); is(scalar(@underlying), 2, 'There were 2 underlying rules'); ok($underlying[0]->specifies_value_for('name'), 'First underlying rule has value for name'); is($underlying[0]->value_for('name'), 'Fred', 'First underlying rule has correct value for for name'); ok(! $underlying[0]->specifies_value_for('foo'), 'First underlying rule correctly has no value for foo'); is_deeply($underlying[0]->value_for('-order'), ['bar'], 'First underlying rule has correct value for -order'); ok(! $underlying[1]->specifies_value_for('name'), 'Second underlying rule correctly has no value for name'); ok($underlying[1]->specifies_value_for('foo'), 'Second underlying rule has value for foo'); is($underlying[1]->value_for('foo'), 'bar', 'Second underlying rule has correct value for for name'); is_deeply($underlying[1]->value_for('-order'), ['bar'], 'Second underlying rule has correct value for -order'); # another compound rule with 3 parts $bx = URT::FancyItem->define_boolexpr(-or => [ [ name => 'Fred' ], [foo => 'bar'], ['score >' => 3 ]], -hints => ['bar','parent_name']); ok($bx, 'Make Or-type rule with -hints'); @underlying = $bx->underlying_rules(); is(scalar(@underlying), 3, 'There were 3 underlying rules'); ok($underlying[0]->specifies_value_for('name'), 'First underlying rule has value for name'); is($underlying[0]->value_for('name'), 'Fred', 'First underlying rule has correct value for for name'); ok(! $underlying[0]->specifies_value_for('foo'), 'First underlying rule correctly has no value for foo'); ok(! $underlying[0]->specifies_value_for('score'), 'First underlying rule correctly has no value for score'); is_deeply($underlying[0]->value_for('-hints'), ['bar','parent_name'], 'First underlying rule has correct value for -hints'); ok(! $underlying[1]->specifies_value_for('name'), 'Second underlying rule correctly has no value for name'); ok($underlying[1]->specifies_value_for('foo'), 'Second underlying rule has value for foo'); is($underlying[1]->value_for('foo'), 'bar', 'Second underlying rule has correct value for for name'); ok(! $underlying[1]->specifies_value_for('score'), 'Second underlying rule correctly has no value for score'); is_deeply($underlying[1]->value_for('-hints'), ['bar','parent_name'], 'Second underlying rule has correct value for -hints'); ok(! $underlying[2]->specifies_value_for('name'), 'Third underlying rule has value for name'); ok(! $underlying[2]->specifies_value_for('foo'), 'Third underlying rule correctly has no value for foo'); ok($underlying[2]->specifies_value_for('score'), 'Third underlying rule has value for score'); is($underlying[2]->value_for('score'), 3, 'Third underlying rule has correct value for for score'); is_deeply($underlying[2]->value_for('-hints'), ['bar','parent_name'], 'Third underlying rule has correct value for -hints'); 03k_flatten_hard_refs.t100664023532023421 1110012544604516 17342 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 4; use Test::Fatal qw(exception); use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; setup(); subtest 'data_type defined on source property' => sub { plan tests => 3; my $property = URT::Show->__meta__->properties(property_name => 'actors'); ok($property->data_type, 'actors has a data_type'); my $actor = URT::Actor->get(name => 'Larry'); my $bx = URT::Show->define_boolexpr(actors => $actor); ok(!scalar(grep { $_ eq 'actors.id' } $bx->params_list), 'unflattend bx does not have id') or diag explain [ $bx->params_list ]; my $flat_bx = $bx->flatten_hard_refs; ok(scalar(grep { $_ eq 'actors.id' } $flat_bx->params_list), 'flattend bx does have id') or diag explain [ $flat_bx->params_list ]; }; subtest 'data_type defined on foreign property' => sub { plan tests => 4; my $shows_property = URT::Actor->__meta__->properties(property_name => 'shows'); ok(!$shows_property->data_type, 'shows does not have a data_type'); ok($shows_property->final_property_meta->data_type, 'shows final_property_meta has a data_type'); my $show = URT::Show->get(name => 'Three Stooges'); my $bx = URT::Actor->define_boolexpr(shows => $show); ok(!scalar(grep { $_ && $_ eq 'shows.id' } $bx->params_list), 'unflattend bx does not have id') or diag explain [ $bx->params_list ]; my $flat_bx = $bx->flatten_hard_refs; ok(scalar(grep { $_ && $_ eq 'shows.id' } $flat_bx->params_list), 'flattend bx does have id') or diag explain [ $flat_bx->params_list ]; }; subtest 'incompatble object type' => sub { plan tests => 1; my $show = URT::Show->get(name => 'Three Stooges'); my $ex = exception { URT::Show->define_boolexpr(actors => $show) }; ok($ex, 'got an exception when trying to use a show as an actor'); }; subtest 'cloned object' => sub { plan tests => 3; my $actor = URT::Actor->get(name => 'Larry'); my $ex = exception { URT::Show->define_boolexpr(actors => $actor) }; ok(!$ex, 'did not get an exception with original actor'); my $clone = UR::Util::deep_copy($actor); isnt("$clone", "$actor", 'cloned actor'); my $bx = URT::Show->define_boolexpr(actors => $clone); $ex = exception { $bx->flatten_hard_refs }; ok($ex, 'got an exception with cloned actor'); }; sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do(q(create table show (id integer PRIMARY KEY, name text))); $dbh->do(q(create table actor (id integer PRIMARY KEY, name text))); $dbh->do(q(create table show_actor_bridge (show_id integer, actor_id integer))); $dbh->do(q(insert into show values (1, 'Three Stooges'))); $dbh->do(q(insert into show values (2, 'Power Rangers'))); $dbh->do(q(insert into actor values (1, 'Larry'))); $dbh->do(q(insert into actor values (2, 'Curly'))); $dbh->do(q(insert into actor values (3, 'Moe'))); $dbh->do(q(insert into actor values (4, 'Black'))); $dbh->do(q(insert into show_actor_bridge values (1, 1))); $dbh->do(q(insert into show_actor_bridge values (1, 2))); $dbh->do(q(insert into show_actor_bridge values (1, 3))); $dbh->do(q(insert into show_actor_bridge values (2, 4))); $dbh->commit(); UR::Object::Type->define( class_name => 'URT::Show', id_by => 'id', has => [ name => { is => 'Text' }, actor_bridges => { is => 'URT::ShowActorBridge', reverse_as => 'show', is_many => 1 }, actors => { is => 'URT::Actor', via => 'actor_bridges', to => 'actor', is_many => 1 }, ], table_name => 'show', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Actor', id_by => 'id', has => [ name => { is => 'Text' }, show_bridges => { is => 'URT::ShowActorBridge', reverse_as => 'actor', is_many => 1 }, shows => { via => 'show_bridges', to => 'show', is_many => 1 }, ], table_name => 'actor', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::ShowActorBridge', id_by => [ show => { is => 'URT::Show', id_by => 'show_id', }, actor => { is => 'URT::Actor', id_by => 'actor_id', }, ], table_name => 'show_actor_bridge', data_source => 'URT::DataSource::SomeSQLite', ); } 03k_rule_for_property_meta_no_properties.t100664023532023421 106212544604516 23415 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 3; use Test::Exception; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; class My::Foo { attributes_have => [ is_blah => { is => 'Boolean' }, ], }; my $meta = My::Foo->__meta__; my @p; lives_ok(sub {@p = $meta->properties(is_blah => 1)}); is(scalar(@p), 0, "didn't get any properties"); dies_ok(sub {@p = $meta->properties(is_blha => 1)}, qr/unknown property is_blha/); 04_datasource_signals.t100664023532023421 1221712544604516 17402 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::FakeDBI; my(@events,@callback_args); # A test datasource package URT::DataSource::Testing; class URT::DataSource::Testing { is => 'URT::DataSource::SomeSQLite' }; sub disconnect { my $self = shift; push @events, 'method:disconnect'; $self->SUPER::disconnect(@_); } sub create_default_handle { my $self = shift; push @events, 'method:create_default_handle'; $self->SUPER::create_default_handle(@_); } { my $use_handle; sub _use_handle { my $self = shift; if (@_) { $use_handle = shift; } return $use_handle; } } sub get_default_handle { my $self = shift; if (my $h = $self->_use_handle) { return $h; } return $self->super_can('get_default_handle')->($self,@_); } *_init_created_dbh = \&init_created_handle; sub init_created_handle { my $self = shift; push @events, 'method:init_created_handle'; $self->SUPER::init_created_handle(@_); } # Fake table/column objects UR::DataSource::RDBMS::Table->__define__( table_name => 'foo', owner => 'main', data_source => 'URT::DataSource::Testing'); UR::DataSource::RDBMS::TableColumn->__define__( column_name => 'test_id', table_name => 'foo', owner => 'main', data_source => 'URT::DataSource::Testing'); foreach my $method ( qw(precreate_handle create_handle predisconnect_handle disconnect_handle query_failed commit_failed) ) { URT::DataSource::Testing->create_subscription( method => $method, callback => sub { my $self = shift; my $reported_method = shift; my @cb_args = @_; for (my $i = 0; $i < @_; $i++) { no warnings 'uninitialized'; $cb_args[$i] =~ s/\n+|\s+/ /sg; $cb_args[$i] =~ s/^\s//; } push @events, "signal:$reported_method"; push @callback_args, \@cb_args; } ); } UR::Object::Type->define( class_name => 'URT::TestingObject', data_source => 'URT::DataSource::Testing', table_name => 'foo', id_by => 'test_id', ); package main; my $dbh = URT::DataSource::Testing->get_default_handle(); ok($dbh, 'get_default_handle()'); is_deeply(\@events, ['signal:precreate_handle', 'method:create_default_handle', 'signal:create_handle', 'method:init_created_handle'], 'signals and methods called in the expected order'); @events = (); ok(URT::DataSource::Testing->disconnect_default_handle(), 'disconnect_default_handle()'); is_deeply(\@events, ['signal:predisconnect_handle', 'method:disconnect', 'signal:disconnect_handle'], 'signals and methods called in the expected order'); # Test the error condition signals my $test_dbh = URT::FakeDBI->new(); URT::DataSource::Testing->_use_handle($test_dbh); URT::DataSource::Testing->dump_error_messages(0); URT::TestingObject->dump_error_messages(0); note('Setting fake handle to fail on prepare()'); $test_dbh->configure('prepare_fail', 'triggering prepare failure'); @events = (); @callback_args = (); eval { URT::TestingObject->get(1) }; is_deeply(\@events, ['signal:query_failed'], 'prepare_failed signal called'); is_deeply(\@callback_args, [ ['prepare', 'select foo.test_id from foo where foo.test_id = ? order by foo.test_id', 'triggering prepare failure'] ], 'query_failed callback given expected args'); note('setting fake handle to fail on execute()'); $test_dbh->configure('prepare_fail', undef); $test_dbh->configure('execute_fail', 'triggering execute failure'); @events = (); @callback_args = (); eval { URT::TestingObject->get(2) }; is_deeply(\@events, ['signal:query_failed'], 'query_failed signal called'); is_deeply(\@callback_args, [ ['execute', 'select foo.test_id from foo where foo.test_id = ? order by foo.test_id', 'triggering execute failure'] ], 'query_failed callback given expected args'); note('setting fake handle to fail on prepare()'); $test_dbh->configure('execute_fail', undef); $test_dbh->configure('prepare_fail', 'prepare fail on commit'); URT::TestingObject->create(3); @events = (); @callback_args = (); UR::Context->current->dump_error_messages(0); ok( ! eval { UR::Context->commit }, 'Commit should fail'); UR::Context->current->dump_error_messages(1); is_deeply(\@events, ['signal:commit_failed'], 'commit_failed signal called'); is_deeply(\@callback_args, [ ['prepare', 'INSERT INTO foo (test_id) VALUES (?)', 'prepare fail on commit'] ], 'commit_failed given expected args'); note('setting fake handle to fail on execute()'); $test_dbh->configure('prepare_fail', undef); $test_dbh->configure('execute_fail', 'execute fail on commit'); @events = (); @callback_args = (); ok( ! eval { UR::Context->commit }, 'Commit should fail'); is_deeply(\@events, ['signal:commit_failed'], 'commit_failed signal called'); is_deeply(\@callback_args, [ ['execute', 'INSERT INTO foo (test_id) VALUES (?)', 'execute fail on commit'] ], 'commit_failed given expected args'); 04_rdbms_retriable_operation.t100664023532023421 1012112544604516 20740 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 25; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::FakeDBI; # A Test datasource # It allows errors with "retry this" to be retried # The DBI component functions are at the bottom package URT::DataSource::Testing; class URT::DataSource::Testing { is => ['UR::DataSource::RDBMSRetriableOperations', 'URT::DataSource::SomeSQLite'], has => [ '_use_handle' ], }; sub get_default_handle { my $self = UR::Util::object(shift); if (my $h = $self->_use_handle) { return $h; } return $self->super_can('get_default_handle')->($self,@_); } sub should_retry_operation_after_error { my($self, $sql, $dbi_errstr) = @_; return scalar($dbi_errstr =~ m/retry this/); } sub default_handle_class { 'URT::FakeDBI' } # The entity we want to try saving package main; class TestThing { id_by => 'test_thing_id', data_source => 'URT::DataSource::Testing', table_name => 'main.test_thing', id_generator => 'test_thing_seq', }; # Fake table/column info for TestThing's table UR::DataSource::RDBMS::Table->__define__( table_name => 'main.test_thing', data_source => 'URT::DataSource::Testing'); UR::DataSource::RDBMS::TableColumn->__define__( column_name => 'test_thing_id', table_name => 'test_thing', data_source => 'URT::DataSource::Testing'); UR::DataSource::RDBMS::PkConstraintColumn->__define__( column_name => 'test_thing_id', table_name => 'main.test_thing', rank => 1, data_source => 'URT::DataSource::Testing'); # # Set up the test # We only want 2 retries... # my $test_ds = TestThing->__meta__->data_source; $test_ds->dump_error_messages(0); $test_ds->retry_sleep_start_sec(0.01); $test_ds->retry_sleep_max_sec(0.03); my $retry_count; my @sleep_counts; $test_ds->add_observer( aspect => 'retry', callback => sub { my($ds, $aspect, $sleep_time) = @_; $retry_count++; push @sleep_counts, $sleep_time; } ); # Try a connection failure retry_test('get_default_handle', 'connect_fail', sub { $test_ds->get_default_handle }); not_retry_test('get_default_handle', 'connect_fail', sub { $test_ds->get_default_handle} ); # Try a get() failure my $test_dbh = URT::FakeDBI->new(); $test_ds->_use_handle($test_dbh); retry_test('get', 'prepare_fail', sub { TestThing->get(1) }); not_retry_test('get', 'prepare_fail', sub { TestThing->get(2) }); # Try a do() failure retry_test('do_sql', 'do_fail', sub { $test_ds->do_sql('select foo from something') }); not_retry_test('do_sql', 'do_fail', sub { $test_ds->do_sql('select foo from something') }); # try a sequence generator retrieval failure # UR::DS::SQLite uses do() to get sequence values retry_test('sequence generator', 'do_fail', sub { TestThing->create() }); not_retry_test('sequence generator', 'do_fail', sub { TestThing->create() }); # try a commit failure UR::Context->dump_error_messages(0); retry_test('commit', 'prepare_fail', sub { TestThing->create(3); UR::Context->commit }); not_retry_test('commit', 'prepare_fail', sub { TestThing->create(4); UR::Context->commit }); sub retry_test { my($label, $dbi_config, $code) = @_; URT::FakeDBI->configure($dbi_config, 'we should retry this'); $retry_count = 0; @sleep_counts = (); eval { $code->() }; like($@, qr(Maximum database retries reached), qq($label: Trapped "max retry" exception)); is($retry_count, 2, "$label retried 2 times"); is_deeply(\@sleep_counts, [0.01,0.02], "$label sleep times"); } sub not_retry_test { my($label, $dbi_config, $code) = @_; URT::FakeDBI->configure($dbi_config, 'fail only once'); $retry_count = 0; eval { $code->() }; my $error_string = $label eq 'commit' ? $test_ds->error_message : $@; like($error_string, qr(fail only once), "$label: non-retriable exception"); is($retry_count, 0, "$label did not retry"); } 04_rdbms_table_name_from_inline_view.t100664023532023421 267112544604516 22404 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace UR::Object::Type->define( class_name => 'URT::NormalTable', id_by => 'id', table_name => 'foo', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::InlineView', id_by => 'id', table_name => '(select foo_id from foo where foo_id is not null) inline_foo', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::InlineViewAs', id_by => 'id', table_name => '(select foo_id from foo where foo_id is not null) as inline_foo', data_source => 'URT::DataSource::SomeSQLite', ); my @tests = ( 'URT::NormalTable' => [undef, undef], 'URT::InlineView' => ['(select foo_id from foo where foo_id is not null)', 'inline_foo'], 'URT::InlineViewAs' => ['(select foo_id from foo where foo_id is not null)', 'inline_foo'], ); for (my $i = 0; $i < @tests; $i += 2) { my($class_name, $expected_data) = @tests[$i, $i+1]; my $class_meta = $class_name->__meta__; my($view, $alias) = URT::DataSource::SomeSQLite->parse_view_and_alias_from_inline_view($class_meta->table_name); is($view, $expected_data->[0], "$class_name view"); is($alias, $expected_data->[1], "$class_name alias"); } 04a_rdbms_retriable_operation-multiple_datasources.t100664023532023421 401112544604516 25310 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t# This documents a bug that didn't actually get shipped that would cause the # default retriable methods on RDBMSRetriableOperations to wrap the first data # source to use it even for subsequent data sources. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; my %_sync_database; *URT::DataSource::SomeSQLiteA::_sync_database = sub { $_sync_database{'URT::DataSource::SomeSQLiteA'}++; return 1; }; *URT::DataSource::SomeSQLiteB::_sync_database = sub { $_sync_database{'URT::DataSource::SomeSQLiteB'}++; return 1; }; my @ds_spec = ( ['URT::DataSource::SomeSQLiteA', 'URT::DataSource::RetriableSQLiteA'], ['URT::DataSource::SomeSQLiteB', 'URT::DataSource::RetriableSQLiteB'], ); my $method = '_sync_database'; for (my $i = 0; $i < @ds_spec; $i++) { my $ds_typename = $ds_spec[$i][0]; my $ds_name = $ds_spec[$i][1]; UR::Object::Type->define( class_name => $ds_typename, is => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => $ds_name, is => [ 'UR::DataSource::RDBMSRetriableOperations', $ds_typename, ], ); } for (my $i = 0; $i < @ds_spec; $i++) { my $ds_typename = $ds_spec[$i][0]; my $ds_name = $ds_spec[$i][1]; my $oi = ($i + 1) % 2; my $other_ds_typename = $ds_spec[$oi][0]; my $other_ds_name = $ds_spec[$oi][1]; setUp(); is(scalar(keys %_sync_database), 0, "$ds_name: setUp OK"); my $ds = $ds_name->get(); my $sync_rv = $ds->$method(); ok($sync_rv, "$ds_name: $method returned successfully"); ok( $_sync_database{$ds_typename}, "$ds_name: this datasource method was called"); ok(!$_sync_database{$other_ds_typename}, "$ds_name: other datasource method was not called"); } sub setUp { for my $k (keys %_sync_database) { delete $_sync_database{$k}; } } 04a_sqlite.t100664023532023421 4150012544604516 15167 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 80; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); &setup_schema($dbh); &test_foreign_key_handling(); &test_column_details(); sub test_column_details { my $schema = URT::DataSource::SomeSQLite->default_owner; my $sth = URT::DataSource::SomeSQLite->get_column_details_from_data_dictionary('',$schema,'inline','%'); my @results; while (my $row = $sth->fetchrow_hashref) { my $saved_row; foreach my $key ( qw( TABLE_NAME COLUMN_NAME DATA_TYPE COLUMN_SIZE NULLABLE COLUMN_DEF ) ) { $saved_row->{$key} = $row->{$key}; } push @results, $saved_row; } @results = sort { $a->{'COLUMN_NAME'} cmp $b->{'COLUMN_NAME'} } @results; my @expected = ( { TABLE_NAME => 'inline', COLUMN_NAME => 'id', DATA_TYPE => 'integer', COLUMN_SIZE => undef, NULLABLE => 1, COLUMN_DEF => undef, }, { TABLE_NAME => 'inline', COLUMN_NAME => 'name', DATA_TYPE => 'varchar', COLUMN_SIZE => 255, NULLABLE => 1, COLUMN_DEF => 'some name', }, ); is_deeply(\@results, \@expected, 'column details for table inline are correct'); } sub test_foreign_key_handling { my $expected_fk_data = &make_expected_fk_data(); my @table_names = qw( foo inline inline_s named named_s unnamed unnamed_s named_2 named_2_s unnamed_2 unnamed_2_s); foreach my $table ( @table_names ) { my $found = &get_fk_info_from_dd('','',$table); my $found_count = scalar(@$found); my $expected = $expected_fk_data->{'from'}->{$table}; my $expected_count = scalar(@$expected); $found = [ sort { $a->{FK_TABLE_NAME} cmp $b->{FK_TABLE_NAME} } @$found ]; $expected = [ sort { $a->{FK_TABLE_NAME} cmp $b->{FK_TABLE_NAME} } @$expected ]; is($found_count, $expected_count, "Number of FK rows from $table is correct"); is_deeply($found, $expected, 'FK data is correct'); } foreach my $table ( @table_names ) { my $found = &get_fk_info_from_dd('','','','','',$table); my $found_count = scalar(@$found); my $expected = $expected_fk_data->{'to'}->{$table}; my $expected_count = scalar(@$expected); $found = [ sort { $a->{UK_TABLE_NAME} cmp $b->{UK_TABLE_NAME} } @$found ]; $expected = [ sort { $a->{UK_TABLE_NAME} cmp $b->{UK_TABLE_NAME} } @$expected ]; is($found_count, $expected_count, "Number of FK rows to $table is correct"); is_deeply($found, $expected, 'FK data is correct'); } } unlink URT::DataSource::SomeSQLite->server; sub setup_schema { my $dbh = shift; ok( $dbh->do('CREATE TABLE foo (id1 integer, id2 integer, PRIMARY KEY (id1, id2))'), 'create table (foo) with 2 primary keys'); ok($dbh->do("CREATE TABLE inline (id integer PRIMARY KEY REFERENCES foo(id1) ON UPDATE RESTRICT ON DELETE SET NULL, name varchar(255) default 'some name')"), 'create table with one inline foreign key to foo'); ok($dbh->do('CREATE TABLE inline_s (id integer PRIMARY KEY REFERENCES foo (id1) ON UPDATE RESTRICT ON DELETE SET NULL , name varchar)'), 'create table with one inline foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE named (id integer PRIMARY KEY, name varchar, CONSTRAINT named_fk FOREIGN KEY (id) REFERENCES foo (id1) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with one named table constraint foreign key to foo'); ok($dbh->do('CREATE TABLE named_s (id integer PRIMARY KEY, name varchar, CONSTRAINT named_s_fk FOREIGN KEY(id) REFERENCES foo (id1) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with one named table constraint foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE unnamed (id integer PRIMARY KEY, name varchar, FOREIGN KEY (id) REFERENCES foo (id1) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with one unnamed table constraint foreign key to foo'); ok($dbh->do('CREATE TABLE unnamed_s (id integer PRIMARY KEY, name varchar, FOREIGN KEY(id) REFERENCES foo(id1) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with one unnamed table constraint foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE named_2 (id1 integer, id2 integer, name varchar, PRIMARY KEY (id1, id2), CONSTRAINT named_2_fk FOREIGN KEY (id1, id2) REFERENCES foo (id1,id2) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with a dual column named foreign key to foo'); ok($dbh->do('CREATE TABLE named_2_s (id1 integer, id2 integer, name varchar, PRIMARY KEY ( id1 , id2 ) , CONSTRAINT named_2_s_fk FOREIGN KEY( id1 , id2 ) REFERENCES foo( id1 , id2 ) ON UPDATE RESTRICT ON DELETE SET NULL )'), 'create table with a dual column named foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE unnamed_2 (id1 integer, id2 integer, name varchar, PRIMARY KEY (id1, id2), FOREIGN KEY (id1, id2) REFERENCES foo (id1,id2) ON UPDATE RESTRICT ON DELETE SET NULL)'), 'create table with a dual column unnamed foreign key to foo'); ok($dbh->do('CREATE TABLE unnamed_2_s (id1 integer, id2 integer, name varchar, PRIMARY KEY( id2 , id2 ) , FOREIGN KEY( id1 , id2 ) REFERENCES foo( id1 , id2 ) ON UPDATE RESTRICT ON DELETE SET NULL )'), 'create table with a dual column unnamed foreign key to foo, with different whitespace'); } sub make_expected_fk_data { my $to = { foo => [], inline => [ { FK_NAME => 'inline_id_foo_id1_fk', FK_TABLE_NAME => 'inline', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], inline_s => [ { FK_NAME => 'inline_s_id_foo_id1_fk', FK_TABLE_NAME => 'inline_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], named => [ { FK_NAME => 'named_fk', FK_TABLE_NAME => 'named', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], named_s => [ { FK_NAME => 'named_s_fk', FK_TABLE_NAME => 'named_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], unnamed => [ { FK_NAME => 'unnamed_id_foo_id1_fk', FK_TABLE_NAME => 'unnamed', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], unnamed_s => [ { FK_NAME => 'unnamed_s_id_foo_id1_fk', FK_TABLE_NAME => 'unnamed_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], named_2 => [ { FK_NAME => 'named_2_fk', FK_TABLE_NAME => 'named_2', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, { FK_NAME => 'named_2_fk', FK_TABLE_NAME => 'named_2', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2', ORDINAL_POSITION => 2, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], named_2_s => [ { FK_NAME => 'named_2_s_fk', FK_TABLE_NAME => 'named_2_s', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, { FK_NAME => 'named_2_s_fk', FK_TABLE_NAME => 'named_2_s', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2', ORDINAL_POSITION => 2, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], unnamed_2 => [ { FK_NAME => 'unnamed_2_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, { FK_NAME => 'unnamed_2_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2', ORDINAL_POSITION => 2, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], unnamed_2_s => [ { FK_NAME => 'unnamed_2_s_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2_s', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1', ORDINAL_POSITION => 1, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, { FK_NAME => 'unnamed_2_s_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2_s', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2', ORDINAL_POSITION => 2, UPDATE_RULE => 1, DELETE_RULE => 2, UK_TABLE_CAT => undef, UK_TABLE_SCHEM => 'main', FK_TABLE_CAT => undef, FK_TABLE_SCHEM => 'main', UK_NAME => undef, DEFERABILITY => undef, }, ], }; # The 'from' data is just the inverse of 'to' my $from; foreach my $fk_list ( values %$to ) { foreach my $fk ( @$fk_list ) { my $uk_table = $fk->{'UK_TABLE_NAME'}; $from->{$uk_table} ||= []; push @{$from->{$uk_table}}, $fk; my $fk_table = $fk->{'FK_TABLE_NAME'}; $from->{$fk_table} ||= []; } } return { from => $from, to => $to }; } sub get_fk_info_from_dd { my $sth = URT::DataSource::SomeSQLite->get_foreign_key_details_from_data_dictionary(@_); { no warnings 'uninitialized'; ok($sth, "Got a sth to get foreign keys from '$_[2]' to '$_[5]'"); } my @rows; while ( my $row = $sth->fetchrow_hashref() ) { push @rows, $row; } return \@rows; } 04a_sqlite_dir_of_schema_files.t100664023532023421 546512544604516 21205 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 3; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use File::Temp; use File::Spec; my $sqlite_dir = File::Temp::tempdir( CLEANUP => 1 ); create_dir_with_schema_files($sqlite_dir); define_classes($sqlite_dir); my $person = URT::Person->get(car_make => 'ford'); is($person->name, 'bob', 'bob owns the ford'); $person = URT::Person->get(car_model => 'model s'); is($person->name, 'fred', 'fred owns the mode s'); $person = URT::Person->get(car_model => 'hupmobile'); ok(!$person, 'no one owns a hupmobile'); sub create_dir_with_schema_files { my$sqlite_dir = shift; my $main_schema_file = File::Spec->catfile($sqlite_dir, 'main.sqlite3'); my $main_dbh = DBI->connect("dbi:SQLite:dbname=$main_schema_file",'','') || die "Can't create main schema file in dir $sqlite_dir: ".$DBI::errstr; $main_dbh->do('create table person (person_id integer primary key, name varchar)'); $main_dbh->do("insert into person values (1, 'bob')"); $main_dbh->do("insert into person values (2, 'fred')"); my $car_schema_file = File::Spec->catfile($sqlite_dir, 'cars.sqlite3'); my $car_dbh = DBI->connect("dbi:SQLite:dbname=$car_schema_file",'','') || die "Can't create cars schema file in dir $sqlite_dir: ".$DBI::errstr; $car_dbh->do('create table car (car_id integer primary_key, owner_id integer not null, make varchar, model varchar)'); $car_dbh->do("insert into car values (1, 1, 'ford','galaxie')"); $car_dbh->do("insert into car values (2, 1, 'chrysler', 'airstream')"); $car_dbh->do("insert into car values (3, 2, 'tesla', 'model s')"); } sub define_classes { my $sqlite_dir = shift; UR::Object::Type->define( class_name => 'URT::DataSource::SQLiteDir', is => 'UR::DataSource::SQLite', has_constant => [ server => { value => $sqlite_dir }, ], ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, cars => { is_many => 1, reverse_as => 'owner', is => 'URT::Car' }, car_make => { via => 'cars', to => 'make' }, car_model => { via => 'cars', to => 'model' }, ], data_source => 'URT::DataSource::SQLiteDir', table_name => 'main.person', ); UR::Object::Type->define( class_name => 'URT::Car', id_by => 'car_id', has => [ owner => { id_by => 'owner_id', is => 'URT::Person' }, make => { is => 'String' }, model => { is => 'String' }, ], data_source => 'URT::DataSource::SQLiteDir', table_name => 'cars.car', ); } 04a_sqlite_examine_unique_indices.t100664023532023421 411212544604516 21737 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use File::Temp; use File::Spec; my $sqlite_dir = File::Temp::tempdir( CLEANUP => 1 ); create_dir_with_schema_files($sqlite_dir); define_datasource($sqlite_dir); my $ds = URT::DataSource::SQLiteDir->get(); my $person_index = $ds->get_unique_index_details_from_data_dictionary('main','person'); my $other_index = $ds->get_unique_index_details_from_data_dictionary('other','person'); is(scalar(keys(%$person_index)), 1, 'found only the index for main schema'); is(scalar(keys(%$other_index)), 1, 'found only the index for other schema'); is((keys(%$person_index))[0], 'main_person_name_idx', 'found proper index for person table'); is((keys(%$other_index))[0], 'other_person_name_idx', 'found proper index for other table'); sub create_dir_with_schema_files { my $sqlite_dir = shift; my $main_schema_file = File::Spec->catfile($sqlite_dir, 'main.sqlite3'); my $main_dbh = DBI->connect("dbi:SQLite:dbname=$main_schema_file",'','') || die "Can't create main schema file in dir $sqlite_dir: ".$DBI::errstr; $main_dbh->do('create table person (person_id integer primary key, name varchar)'); $main_dbh->do("create unique index main_person_name_idx ON person (name)"); my $other_schema_file = File::Spec->catfile($sqlite_dir, 'other.sqlite3'); my $other_dbh = DBI->connect("dbi:SQLite:dbname=$other_schema_file",'','') || die "Can't create other schema file in dir $sqlite_dir: ".$DBI::errstr; $other_dbh->do('create table person (person_id integer primary_key, name varchar)'); $other_dbh->do("create unique index other_person_name_idx ON person (name)"); } sub define_datasource { my $sqlite_dir = shift; UR::Object::Type->define( class_name => 'URT::DataSource::SQLiteDir', is => 'UR::DataSource::SQLite', has_constant => [ server => { value => $sqlite_dir }, ], ); } 04a_sqlite_init_db_internal.t100775023532023421 602512544604516 20541 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Test the internal dumping code for systems that do not have sqlite3 in the PATH my @db_creation_text = ( q(BEGIN TRANSACTION;), q(CREATE TABLE bar (bar_id integer PRIMARY KEY, some_data varchar);), q(INSERT INTO bar VALUES(1,'Hi there');), q(INSERT INTO bar VALUES(2,'blahblah');), q(INSERT INTO bar VALUES(3,null);), q(CREATE TABLE foo (foo_id_1 integer, foo_id_2 integer, PRIMARY KEY (foo_id_1, foo_id_2));), q(INSERT INTO foo VALUES(1,2);), q(INSERT INTO foo VALUES(2,3);), q(INSERT INTO foo VALUES(4,5);), q(COMMIT;), ); if (defined URT::DataSource::SomeSQLite->_singleton_object->_get_foreign_key_setting) { # If DBD::SQLite supports foreign keys, then the dump file will have this line unshift @db_creation_text, q(PRAGMA foreign_keys = OFF;); plan tests => 21; } else { plan tests => 20; } my $dump_file = URT::DataSource::SomeSQLite->_data_dump_path(); my $fh = IO::File->new($dump_file, 'w'); ok($fh, "Opened dump file for writing"); unless ($fh) { diag "Can't open $dump_file for writing: $!"; } $fh->print(join("\n", @db_creation_text), "\n"); $fh->close(); { local $ENV{'PATH'} = '/nonexistent'; # These _should_ ensure that we'll re-initialize the DB from the dump my $db_file = URT::DataSource::SomeSQLite->server; unlink($db_file); URT::DataSource::SomeSQLite->disconnect; note("initializing DB"); URT::DataSource::SomeSQLite->_init_database(); note("db file is $db_file"); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); # Try getting some data my @row = $dbh->selectrow_array('select * from foo where foo_id_1 = 1'); ok(($row[0] == 1 and $row[1] == 2), 'Got row from table foo'); @row = $dbh->selectrow_array('select * from foo where foo_id_1 = 2'); ok(($row[0] == 2 and $row[1] == 3), 'Got row from table foo'); @row = $dbh->selectrow_array('select * from bar where bar_id = 1'); ok(($row[0] == 1 and $row[1] eq 'Hi there'), 'Got row from table bar'); @row = $dbh->selectrow_array('select * from bar where bar_id = 3'); ok(($row[0] == 3 and !defined($row[1])) , 'Got row from table bar'); # truncate the dump file to 0 bytes { my $fh = IO::File->new($dump_file, '>'); $fh->close(); } ok(URT::DataSource::SomeSQLite->_singleton_object->_dump_db_to_file_internal(), 'Call force re-creation of the dump file'); ok((-r $dump_file and -s $dump_file), 'Re-created dump file'); $fh = IO::File->new($dump_file); ok($fh, "Opened dump file for reading"); for(my $i = 0; $i < @db_creation_text; $i++) { my $line = $fh->getline(); chomp $line; is($line, $db_creation_text[$i], 'DB dump test line ' . ($i+1) . ' is correct'); } } 04a_sqlite_sync_database.t100664023532023421 1172412544604516 20054 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 30; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); # Make 3 tables, one with lower case names, one with upper, one with mixed case # do some CRUD and then commit(). Make sure the real data got saved and the # metadata is created correctly ok($dbh->do('create table person (person_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create person table'); ok($dbh->do('create table EMPLOYEE (EMPLOYEE_ID integer NOT NULL PRIMARY KEY references person(person_id), OFFICE varchar)'), 'create EMPLOYEE table'); ok($dbh->do('create table InvenTory (InvenToryId integer NOT NULL PRIMARY KEY, Owner integer references EMPLOYEE(EMPLOYEE_ID), Name varchar)'), 'create InvenTory table'); # insert some data ok($dbh->do("insert into person values (100, 'UpdateName')"), 'insert person'); ok($dbh->do("insert into person values (101, 'DoNotChange')"), 'insert person'); ok($dbh->do("insert into person values (102, 'DeleteName')"), 'insert person'); ok($dbh->do("insert into person values (103, 'GetByJoin')"), 'insert person'); ok($dbh->do("insert into EMPLOYEE values (100, 'office 100')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (101, 'office 101')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (102, 'office 102')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (103, 'GetByJoin')"), 'insert person'); # person ID 100 has a black car and a red stapler # person ID 101 has a green chair and green phone # person ID 102 has nothing to begin with # person ID 103 has an item called 'Join' ok($dbh->do("insert into InvenTory values (100, 100, 'black car')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (101, 100, 'red stapler')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (102, 101, 'greep chair')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (103, 101, 'green phone')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (104, 103, 'Join')"), 'insert InvenTory'); # And now class definitions for those 3 tables UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', is_abstract => 1, id_by => 'person_id', has => ['name'], ); UR::Object::Type->define( class_name => 'URT::Inventory', data_source => 'URT::DataSource::SomeSQLite', table_name => 'InvenTory', id_by => 'InvenToryId', has => [ owner_id => { is => 'Integer', column_name => 'Owner' }, owner => { is => 'URT::Employee', id_by => 'owner_id' }, name => { is => 'String', column_name => 'Name' }, ], ); UR::Object::Type->define( class_name => 'URT::Employee', data_source => 'URT::DataSource::SomeSQLite', table_name => 'EMPLOYEE', is => 'URT::Person', id_by => 'EMPLOYEE_ID', has => [ office => { is => 'String', column_name => 'OFFICE' }, inventory => { is => 'URT::Inventory', reverse_as => 'owner', is_many => 1 }, ], ); my @sql = (); URT::DataSource::SomeSQLite->add_observer( aspect => 'query', callback => sub { my($data_source, $method, $sql) = @_; if ($method eq 'query') { $sql =~ s/^\s+|\s+$//g; # remove leading and trailing whitespace $sql =~ s/\s+/ /g; # change whitespace to a single space push(@sql, $sql); } } ); @sql = (); my $person = URT::Employee->get(name => 'NotThere'); ok(!$person, 'Get employee by name failed for non-existent name'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select EMPLOYEE.EMPLOYEE_ID, EMPLOYEE.OFFICE, person.name, person.person_id from EMPLOYEE INNER join person on EMPLOYEE.EMPLOYEE_ID = person.person_id where person.name = ? order by EMPLOYEE.EMPLOYEE_ID', 'SQL is correct'); @sql = (); $person = URT::Employee->get(name => 'UpdateName'); ok($person, 'Get employee by name worked'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select EMPLOYEE.EMPLOYEE_ID, EMPLOYEE.OFFICE, person.name, person.person_id from EMPLOYEE INNER join person on EMPLOYEE.EMPLOYEE_ID = person.person_id where person.name = ? order by EMPLOYEE.EMPLOYEE_ID', 'SQL is correct'); @sql = (); ok($person->name('Changed'), 'Change name for person'); is(scalar(@sql), 0, 'Made no queries'); @sql = (); my @inventory = $person->inventory(); is(scalar(@inventory), 2, 'That person has 2 inventory items'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select InvenTory.InvenToryId, InvenTory.Name, InvenTory.Owner from InvenTory where InvenTory.Owner = ? order by InvenTory.InvenToryId', 'SQL is correct'); @sql = (); $person = URT::Employee->get(name => 'DeleteName'); ok($person, 'Got Employee by name'); 04b_mysql.t100664023532023421 62612544604516 15000 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring MySQL"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $dbh = URT::DataSource::SomeMySQL->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 04b_rdbms_retriable_operation-recursion_bug.t100664023532023421 260512544604516 23736 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t# This documents a bug in a prior implementation of RDBMSRetriableOperations's # rdbms_datasource_method_for that would get stuck in an infinite loop if there # were any intermediate classes in the inheritance between the actual data # source and the inheritance of RDBMSRetriableOperations. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; package URT::DataSource::RetriableSQLite; class URT::DataSource::RetriableSQLite { is => [ 'UR::DataSource::RDBMSRetriableOperations', 'URT::DataSource::SomeSQLite', ], }; package URT::DataSource::RetryDBWithoutOverride; class URT::DataSource::RetryDBWithoutOverride { is => 'URT::DataSource::RetriableSQLite', doc => 'no _sync_database override', }; package URT::DataSource::RetryDBWithOverride; class URT::DataSource::RetryDBWithOverride { is => 'URT::DataSource::RetriableSQLite', doc => 'with _sync_database override', }; sub _sync_database { my $self = shift; $self->SUPER::_sync_database(@_); } package main; use Test::More tests => 2; for my $ds_name (qw( URT::DataSource::RetryDBWithOverride URT::DataSource::RetryDBWithoutOverride )) { my $ds = $ds_name->get(); my $sync_rv = $ds->_sync_database(); ok($sync_rv, "$ds_name: _sync_database returned successfully"); } 04c_postresql.t100664023532023421 66212544604516 15670 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring PostgreSQL"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomePostgreSQL->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 04c_postresql_type_coercion.t100664023532023421 377312544604516 20640 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # This tests the PostgreSQL data source's ability to filter a date-type column # with "like" and a string. UR::Object::Type->define( class_name => 'URT::A', id_by => [ a_id => { is => 'String' }, ], has => [ creation_date => { is => 'DateTime' }, some_event_time => { is => 'Timestamp' }, ], table_name => 'A', data_source => 'URT::DataSource::SomePostgreSQL', ); { my $sql = ''; URT::DataSource::SomePostgreSQL->add_observer( aspect => 'query', callback => sub { my $ds = shift; my $aspect = shift; $sql = shift; $sql =~ s/\n/ /g; # Convert newlines to spaces $sql =~ s/^\s+|\s+$//g; # Remove leading and trailing whitespace # We need to die here so it dosen't try to connect to this # fake oracle database, which happens right after the SQL is # constructed die "escape\n"; } ); sub get_sql { my $code = shift; $sql = ''; eval { $code->() }; unless ($@ =~ m/escape/) { ok(0, "Did not capture the SQL, got $@"); exit; } return $sql; } } is(get_sql(sub { URT::A->get('creation_date like' => '1999-12-31%') }), q{select A.a_id, A.creation_date, A.some_event_time from A where to_char(A.creation_date, 'YYYY-MM-DD HH24:MI:SS') like ? escape E'\\\\' order by A.a_id COLLATE "C"}, "to_char coercion on DateTime column"); is(get_sql(sub { URT::A->get('some_event_time like' => '1970-01-01%') }), q{select A.a_id, A.creation_date, A.some_event_time from A where to_char(A.some_event_time, 'YYYY-MM-DD HH24:MI:SS.US') like ? escape E'\\\\' order by A.a_id COLLATE "C"}, "to_char coercion on Timestamp column"); 04d_oracle.t100664023532023421 65212544604516 15101 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring Oracle"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeOracle->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 04d_oracle_join_coercion.t100664023532023421 3007112544604516 20037 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 20; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # This tests the Oracle data source's ability to join a text-type column # to a non-text type, like a number or date. Oracle versions 10 and under # seemed to be more permissive when joining dissimilar columns. Version # 11 rejects queries that worked before. # # Make a few classes that can join to each other, numbers and text UR::Object::Type->define( class_name => 'URT::A', id_by => [ a_id => { is => 'String' }, ], has => [ age => { is => 'Number' }, b_id => { is => 'Number' }, b => { is => 'URT::B', id_by => 'b_id' }, b_name => { via => 'b', to => 'name' }, ], table_name => 'A', data_source => 'URT::DataSource::SomeOracle', ); UR::Object::Type->define( class_name => 'URT::AChild', is => 'URT::A', id_by => [ a_id => { is => 'Number' }, ], table_name => 'A_CHILD', data_source => 'URT::DataSource::SomeOracle', ); UR::Object::Type->define( class_name => 'URT::B', id_by => [ b_id => { is => 'String' }, ], has => [ a_id => { is => 'String' }, a_child => { is => 'URT::AChild', id_by => 'a_id' }, name => { is => 'String' }, ], table_name => 'B', data_source => 'URT::DataSource::SomeOracle', ); my $sql = ''; URT::DataSource::SomeOracle->add_observer( aspect => 'query', callback => sub { my $ds = shift; my $aspect = shift; $sql = shift; $sql =~ s/\n/ /g; # Convert newlines to spaces $sql =~ s/^\s+|\s+$//g; # Remove leading and trailing whitespace # We need to die here so it dosen't try to connect to this # fake oracle database, which happens right after the SQL is # constructed die "escape\n"; } ); # URT::A and URT::AChild, when joined, will have the to_char # coercion on the left. Joining to B will also have the coercion # on the left $sql = ''; eval { URT::AChild->get(1) }; like($@, qr(escape), 'Query on AChild'); is($sql, q{select A_CHILD.a_id, A.a_id, A.age, A.b_id from A_CHILD INNER join A on to_char(A_CHILD.a_id) = A.a_id where A_CHILD.a_id = ? order by A_CHILD.a_id}, "to_char coercion on A_CHILD's ID column for inheritance on the left"); $sql = ''; eval { URT::A->get(b_name => 'foo') }; like($@, qr(escape), 'Query on A, filter by b_name'); is($sql, q{select A.a_id, A.age, A.b_id, b_name_1.a_id, b_name_1.b_id, b_name_1.name from A LEFT join B b_name_1 on to_char(A.b_id) = b_name_1.b_id where b_name_1.name = ? order by A.a_id}, "to_char coercion for A's B_ID column for via/to on the left"); $sql = ''; eval { URT::AChild->get(b_name => 'foo') }; like($@, qr(escape), 'Query on A, filter by b_name'); is($sql, q{select A_CHILD.a_id, A.a_id, A.age, A.b_id, b_name_1.a_id, b_name_1.b_id, b_name_1.name from A_CHILD INNER join A on to_char(A_CHILD.a_id) = A.a_id LEFT join B b_name_1 on to_char(A.b_id) = b_name_1.b_id where b_name_1.name = ? order by A_CHILD.a_id}, "to_char coercion on A_CHILD's ID column and A's B_ID column are both on the left"); $sql = ''; eval { URT::B->get('a_child.age' => 'foo') }; like($@, qr(escape), 'Query on B, filter by a_child.age'); is($sql, q{select B.a_id, B.b_id, B.name, a_child_age_1.a_id, a_child_age_2.a_id, a_child_age_2.age, a_child_age_2.b_id from B LEFT join A_CHILD a_child_age_1 on B.a_id = to_char(a_child_age_1.a_id) LEFT join A a_child_age_2 on to_char(a_child_age_1.a_id) = a_child_age_2.a_id where a_child_age_2.age = ? order by B.b_id}, "to_char coercion on B's a_id column for via/to on the right, and A_CHILD's inheritance on the left"); $sql = ''; # Kind of a nonsense query... will join through A_CHILD, A and back to B eval { URT::B->get('a_child.b.name' => 'foo') }; like($@, qr(escape), 'Query on B, filter by a_child.b.name'); is($sql, q{select B.a_id, B.b_id, B.name, a_child_b_name_1.a_id, a_child_b_name_2.a_id, a_child_b_name_2.age, a_child_b_name_2.b_id, a_child_b_name_3.a_id, a_child_b_name_3.b_id, a_child_b_name_3.name from B LEFT join A_CHILD a_child_b_name_1 on B.a_id = to_char(a_child_b_name_1.a_id) LEFT join A a_child_b_name_2 on to_char(a_child_b_name_1.a_id) = a_child_b_name_2.a_id LEFT join B a_child_b_name_3 on to_char(a_child_b_name_2.b_id) = a_child_b_name_3.b_id where a_child_b_name_3.name = ? order by B.b_id}, "to_char coerction on the right for B's via/to A, and left for A_CHILD's inheritance and A's via/to B"); UR::Object::Type->define( class_name => 'URT::Activity', id_by => [ date => { is => 'DateTime' }, ], has => [ description => { is => 'String' }, ], has_many => [ bridges => { is => 'URT::ThingActivityBridge', reverse_as => 'activity' }, things => { is => 'URT::Thing', via => 'bridges', to => 'thing' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'ACTIVITY', ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Number' }, ], has => [ name => { is => 'String' }, latest_date => { is => 'String' }, latest_activity => { is => 'URT::Activity', id_by => 'latest_date' }, latest_activity_description => { via => 'latest_activity', to => 'description' }, ], has_many => [ bridges => { is => 'URT::ThingActivityBridge', reverse_as => 'thing' }, activities => { is => 'URT::Activity', via => 'bridges', to => 'activity' }, activity_descriptions => { via => 'activities', to => 'description' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'THING', ); UR::Object::Type->define( class_name => 'URT::ThingActivityBridge', id_by => [ thing_id => { is => 'String' }, date => { is => 'String' }, ], has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, activity => { is => 'URT::Activity', id_by => 'date' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'BRIDGE', ); $sql = ''; eval { URT::Thing->get(-hints => ['latest_activity_description']) }; like($@, qr(escape), 'Query on Thing, -hint on latest_activity_description'); is($sql, q{select THING.latest_date, THING.name, THING.thing_id, latest_activity_description_1.date, latest_activity_description_1.description from THING LEFT join ACTIVITY latest_activity_description_1 on THING.latest_date = to_char(latest_activity_description_1.date, 'YYYY-MM-DD HH24:MI:SS') order by THING.thing_id}, "to_char coercion used when joining ACTIVITY's date column to THING's latest_date column"); $sql = ''; eval { URT::Thing->get('activity_descriptions like' => '%cool%') }; like($@, qr(escape), 'Query on Thing, filter on activity_descriptions like %cool%'); is($sql, q{select THING.latest_date, THING.name, THING.thing_id, bridges_1.date, bridges_1.thing_id, activity_descriptions_2.date, activity_descriptions_2.description from THING LEFT join BRIDGE bridges_1 on to_char(THING.thing_id) = bridges_1.thing_id LEFT join ACTIVITY activity_descriptions_2 on bridges_1.date = to_char(activity_descriptions_2.date, 'YYYY-MM-DD HH24:MI:SS') where activity_descriptions_2.description like ? escape '\' order by THING.thing_id}, "to_char coercion present joining THING to BRIDGE by thing_id, and joining BRIDGE to ACTIVITY by date"); # These are the same classes as immediatly above, but URT::Activity2::date is a Timestamp # instead of DateTime UR::Object::Type->define( class_name => 'URT::Activity2', id_by => [ date => { is => 'Timestamp' }, ], has => [ description => { is => 'String' }, ], has_many => [ bridges => { is => 'URT::ThingActivityBridge2', reverse_as => 'activity' }, things => { is => 'URT::Thing2', via => 'bridges', to => 'thing' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'ACTIVITY', ); UR::Object::Type->define( class_name => 'URT::Thing2', id_by => [ thing_id => { is => 'Number' }, ], has => [ name => { is => 'String' }, latest_date => { is => 'String' }, latest_activity => { is => 'URT::Activity2', id_by => 'latest_date' }, latest_activity_description => { via => 'latest_activity', to => 'description' }, ], has_many => [ bridges => { is => 'URT::ThingActivityBridge2', reverse_as => 'thing' }, activities => { is => 'URT::Activity2', via => 'bridges', to => 'activity' }, activity_descriptions => { via => 'activities', to => 'description' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'THING', ); UR::Object::Type->define( class_name => 'URT::ThingActivityBridge2', id_by => [ thing_id => { is => 'String' }, date => { is => 'String' }, ], has => [ thing => { is => 'URT::Thing2', id_by => 'thing_id' }, activity => { is => 'URT::Activity2', id_by => 'date' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'BRIDGE', ); $sql = ''; eval { URT::Thing2->get(-hints => ['latest_activity_description']) }; like($@, qr(escape), 'Query on Thing, -hint on latest_activity_description'); is($sql, q{select THING.latest_date, THING.name, THING.thing_id, latest_activity_description_1.date, latest_activity_description_1.description from THING LEFT join ACTIVITY latest_activity_description_1 on THING.latest_date = to_char(latest_activity_description_1.date, 'YYYY-MM-DD HH24:MI:SSXFF') order by THING.thing_id}, "to_char coercion used when joining ACTIVITY's date column to THING's latest_date column"); $sql = ''; eval { URT::Thing2->get('activity_descriptions like' => '%cool%') }; like($@, qr(escape), 'Query on Thing, filter on activity_descriptions like %cool%'); is($sql, q{select THING.latest_date, THING.name, THING.thing_id, bridges_1.date, bridges_1.thing_id, activity_descriptions_2.date, activity_descriptions_2.description from THING LEFT join BRIDGE bridges_1 on to_char(THING.thing_id) = bridges_1.thing_id LEFT join ACTIVITY activity_descriptions_2 on bridges_1.date = to_char(activity_descriptions_2.date, 'YYYY-MM-DD HH24:MI:SSXFF') where activity_descriptions_2.description like ? escape '\' order by THING.thing_id}, "to_char coercion present joining THING to BRIDGE by thing_id, and joining BRIDGE to ACTIVITY by date"); # Test a join where the get() target and the class it joins to do not # have tables, but their parent do. Also, the joined columns have different names UR::Object::Type->define( class_name => 'URT::LinkParent', id_by => [ id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'LINK', ); UR::Object::Type->define( class_name => 'URT::Link', is => 'URT::LinkParent' ); UR::Object::Type->define( class_name => 'URT::AbstractParent', id_by => [ parent_id => { is => 'String' }, ], has => [ link_id => { is => 'String' }, link => { is => 'URT::Parent', id_by => 'link_id' }, link_name => { via => 'link', to => 'name' }, ], data_source => 'URT::DataSource::SomeOracle', table_name => 'ABSTRACT_PARENT', ); UR::Object::Type->define( class_name => 'URT::Concrete', is => 'URT::AbstractParent', has => [ # Child overrides parent property with more specific 'is' link => { is => 'URT::Link', id_by => 'link_id' }, something => { is => 'String' } ] ); $sql = ''; eval { URT::Concrete->get(-hints => ['link']) }; like($@, qr(escape), 'Query on Thing, filter on activity_descriptions like %cool%'); is($sql, q(select ABSTRACT_PARENT.link_id, ABSTRACT_PARENT.parent_id, link_2.id, link_2.name from ABSTRACT_PARENT LEFT join LINK link_2 on ABSTRACT_PARENT.link_id = to_char(link_2.id) order by ABSTRACT_PARENT.parent_id), 'to_char conversion and correct column linking when joining child classes that do not have tables'); 1; 04e_file.t100664023532023421 1006112544604516 14607 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 54; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace # FIXME - this doesn't test the UR::DataSource::File internals like seeking and caching my $ds = URT::DataSource::SomeFile->get(); my $filename = $ds->server; ok($filename, 'URT::DataSource::SomeFile has a server'); unlink $filename if -f $filename; my $rs = $ds->record_separator; our @data = ( [ 1, 'Bob', 'blue' ], [ 2, 'Fred', 'green' ], [ 3, 'Joe', 'red' ], [ 4, 'Frank', 'yellow' ], ); &setup($ds); my $fh = $ds->get_default_handle(); ok($fh, "got a handle"); isa_ok($fh, 'IO::Handle', 'Returned handle is the proper class'); my $thing = URT::Things->get(thing_name => 'Fred'); ok($thing, 'singular get() returned an object'); is($thing->id, 2, 'object id is correct'); is($thing->thing_id, 2, 'thing_id is correct'); is($thing->thing_name, 'Fred', 'thing_name is correct'); is($thing->thing_color, 'green', 'thing_color is correct'); #my @things = URT::Things->get('thing_color ne' => 'red'); my @things = URT::Things->get(thing_color => {operator => 'not in', value => ['red','green']}); is(scalar(@things), 2, 'Get where color ne "red" returned 3 items'); @things = URT::Things->get(thing_color => { operator => 'like', value => 'ye%o%' }); is(scalar(@things), 1, 'Returned one thing for "thing_color like" "ye%o%"'); is($things[0]->thing_name, 'Frank', 'It was the right thing'); @things = URT::Things->get(); is(scalar(@things), scalar(@data), 'multiple get() returned the right number of objects'); for (my $i = 0; $i < @data; $i++) { # They should get returned in the same order, since @data is sorted is($things[$i]->thing_id, $data[$i]->[0], "Object $i thing_id is correct"); is($things[$i]->thing_name, $data[$i]->[1], "Object $i thing_name is correct"); is($things[$i]->thing_color, $data[$i]->[2], "Object $i thing_color is correct"); } my $iter1 = URT::Things->create_iterator(); my $iter2 = URT::Things->create_iterator(); for (my $i = 0; $i < @data; $i++) { my $obj = $iter1->next(); is($obj->thing_id, $data[$i]->[0], 'Iterator 1, thing_id is correct'); is($obj->thing_name, $data[$i]->[1], 'Iterator 1, thing_name is correct'); is($obj->thing_color, $data[$i]->[2], 'Iterator 1, thing_color is correct'); $obj = $iter2->next(); is($obj->thing_id, $data[$i]->[0], 'Iterator 2, thing_id is correct'); is($obj->thing_name, $data[$i]->[1], 'Iterator 2, thing_name is correct'); is($obj->thing_color, $data[$i]->[2], 'Iterator 2, thing_color is correct'); } my $obj = $iter1->next(); ok(! defined($obj), 'Iterator 1 returns undef when all data is exhausted'); $obj = $iter2->next(); ok(! defined($obj), 'Iterator 2 returns undef when all data is exhausted'); my $fh2 = $ds->get_default_handle(); my $thing1 = URT::Things->get(thing_name => 'FredX'); my $pid = UR::Context::Process->fork(); if ($pid) { my $thing2= URT::Things->get(thing_name => 'FredY'); ok(!$thing2, "correctly failed to get something we didn't expect to see"); ok(URT::Things->get(thing_color=>'yellow'), "got something we did expect to see, even after forking"); waitpid($pid, 0); } else { exit(0); } unlink URT::DataSource::SomeFile->server; sub setup { my $ds = shift; my $filename = $ds->server; my $fh = IO::File->new($filename, '>'); ok($fh, 'opened file for writing'); my $delimiter = $ds->delimiter; my $rs = $ds->record_separator; foreach my $line ( @data ) { $fh->print(join($delimiter, @$line),$rs); } $fh->close; my $c = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, ], table_name => 'FILE', data_source => 'URT::DataSource::SomeFile' ); ok($c, 'Created class'); } 1; 04e_file_sync_database.t100664023532023421 603212544604516 17452 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 20; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace # FIXME - this doesn't test the UR::DataSource::File internals like seeking and caching my $ds = URT::DataSource::SomeFile->get(); ok($ds, 'Got SomeFile data source'); &setup($ds); my $thing1 = URT::Things->get(thing_name => 'Fred'); ok($thing1, 'singular get() returned an object'); ok($thing1->thing_color('blueish'), 'Changed color'); my $thing2 = URT::Things->get(thing_name => 'Frank'); ok($thing2->thing_name('Anonymous'), 'Changed name on a different thing'); my $thing3 = URT::Things->get(thing_name => 'Joe'); ok($thing3->delete, 'Deleted a third thing'); my $new_thing1 = URT::Things->create(thing_id => 3, thing_name => 'Newby', thing_color=> 'clear'); ok($new_thing1, 'created new thing'); ok(!exists($new_thing1->{'db_committed'}), "New thing correctly has no 'db_committed' hash key"); my $new_thing2 = URT::Things->create(thing_id => 0, thing_name => 'Something', thing_color => 'white'); ok($new_thing2, 'created new thing 2'); my $new_thing3 = URT::Things->create(thing_id => 10, thing_name => 'Bobish', thing_color => 'redish'); ok($new_thing3, 'created new thing 3'); ok(UR::Context->commit, 'Commit'); &check_file($ds); ok(exists($new_thing1->{'db_committed'}), "New thing 1 now has a 'db_committed' has key"); unlink $ds->server; sub setup { my $ds = shift; my $filename = $ds->server; my $delimiter = $ds->delimiter; my $rs = $ds->record_separator; ok($filename, 'URT::DataSource::SomeFile has a server'); unlink $filename if -f $filename; my @data = ( [ 1, 'Bob', 'blue' ], [ 2, 'Fred', 'green' ], [ 4, 'Joe', 'red' ], [ 5, 'Frank', 'yellow' ], ); my $fh = IO::File->new($filename, '>'); ok($fh, 'opened file for writing'); foreach my $line ( @data ) { $fh->print(join($delimiter, @$line),$rs); } $fh->close; my $c = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, ], table_name => 'FILE', data_source => 'URT::DataSource::SomeFile' ); ok($c, 'Created class'); } sub check_file { my $ds = shift; my $fh = IO::File->new($ds->server); my $line = $fh->getline(); is($line, qq(0\tSomething\twhite\n), 'Line 0 ok'); $line = $fh->getline(); is($line, qq(1\tBob\tblue\n), 'Line 1 ok'); $line = $fh->getline(); is($line, qq(2\tFred\tblueish\n), 'Line 2 ok'); $line = $fh->getline(); is($line, qq(3\tNewby\tclear\n), 'Line 3 ok'); $line = $fh->getline(); is($line, qq(5\tAnonymous\tyellow\n), 'Line 4 ok'); $line = $fh->getline(); is($line, qq(10\tBobish\tredish\n), 'Line 5 ok'); } 1; 04e_file_track_open_close.t100664023532023421 2571412544604516 20214 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 100; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace use File::Temp; # The file tracking stuff is defined at the bottom of this file my ($file_new, $file_open, $file_close, $file_DESTROY, $file_seek, $file_seek_pos, $file_tell); IO::File::Tracker->config_callbacks( 'new' => sub { no warnings 'uninitialized'; $file_new++ }, 'open' => sub { no warnings 'uninitialized'; $file_open++ }, 'close' => sub { no warnings 'uninitialized'; $file_close++ }, 'DESTROY' => sub { no warnings 'uninitialized'; $file_DESTROY++ }, 'seek' => sub { no warnings 'uninitialized'; $file_seek_pos = $_[0]; $file_seek++ }, 'tell' => sub { no warnings 'uninitialized'; $file_tell++ }, ); sub clear_trackers { $file_new = 0; $file_open = 0; $file_close = 0; $file_DESTROY = 0; $file_seek = 0; $file_seek_pos = undef; $file_tell = 0; }; my $file_line_length = 8; # Includes the newline my $file_data = qq(1\tAAA\t1 2\tBBB\t1 3\tCCC\t1 4\tDDD\t1 5\tEEE\t1 6\tfff\t0 7\tggg\t0 8\thhh\t0 9\tiii\t0 ); # First, make up a File datasource with the default behavior of keeping its file # handle open as long as possible my(undef,$tempfile_name_1) = File::Temp::tempfile(); END { unlink $tempfile_name_1 } my $fh_1 = IO::File->new($tempfile_name_1, 'w'); $fh_1->print($file_data); $fh_1->close(); my $keepopen_ds = UR::DataSource::File->create( delimiter => "\t", quick_disconnect => 0, handle_class => 'IO::File::Tracker', server => $tempfile_name_1, column_order => ['letter_id', 'name', 'is_upper'], sort_order => ['letter_id'], ); UR::Object::Type->define( class_name => 'URT::Letters', id_by => 'letter_id', has => [ letter_id => { is => 'Integer' }, name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $keepopen_ds->id, ); &clear_trackers(); my $obj = URT::Letters->get(1); ok($obj, 'Got an object from the file'); is($obj->name, 'AAA', 'it has the correct name'); ok($file_new, 'new() was called on the file handle'); ok($file_open, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = URT::Letters->get(2); ok($obj, 'Got second object from the file'); is($obj->name, 'BBB', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = URT::Letters->get(5); ok($obj, 'Got fifth object from the file'); is($obj->name, 'EEE', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() was to the correct position'); # This one should still be in the data source's cache &clear_trackers(); $obj = URT::Letters->get(4); ok($obj, 'Got fourth object'); is($obj->name, 'DDD', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() was to the correct position'); # This datasource points to the same file (not a problem since we're not writing to it) # but with the quick_disconnect flag on my $close_ds = UR::DataSource::File->create( delimiter => "\t", quick_disconnect => 1, handle_class => 'IO::File::Tracker', server => $tempfile_name_1, column_order => ['letter_id', 'name', 'is_upper'], sort_order => ['letter_id'], ); UR::Object::Type->define( class_name => 'URT::LettersAlternate', id_by => 'letter_id', has => [ letter_id => { is => 'Integer' }, name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $close_ds->id, ); # Create a couple of iterators on the same datasource and interleave their # reads, and make sure they seek back to the correct positions &clear_trackers(); my $lower_iter = URT::LettersAlternate->create_iterator(is_upper => 0); ok($lower_iter, 'Created an iterator for lower case objects'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got an object from the lower case iterator'); is($obj->name, 'fff', 'It was the first lowercase object'); is($file_new, 1, 'new() was called on the file handle'); is($file_open, 1, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got another object from the lower case iterator'); is($obj->name, 'ggg', 'It was the next lowercase object'); is($file_new, 0, 'new() was called on the file handle'); is($file_open, 0, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); # This get() won't close the handle because $all_iter is still running $obj = URT::LettersAlternate->get(9); ok($obj, 'Use get() to get the ninth object'); is($obj->name, 'iii', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 7, 'seek() set the file pos to the 7th line'); # Because the lower-case iter gets us this far &clear_trackers(); my $upper_iter = URT::LettersAlternate->create_iterator(is_upper => 1); ok($upper_iter, 'Created an iterator for upper case objects'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'AAA', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() set the file pos to 0'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got an object from the lower case iterator'); is($obj->name, 'hhh', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 8, 'seek() set the file pos to the 8th line'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'BBB', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() set the file pos to the 1th (second) line'); &clear_trackers(); $lower_iter = undef; #diag('Closing the lower case object iterator'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'CCC', 'It was the third object'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $upper_iter = undef; #diag('Closing the upper case object iterator'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 1, 'close() was called on the file handle'); is($file_seek, 0, 'seek() was called on the file handle'); &clear_trackers(); $obj = URT::LettersAlternate->get(5); # something not in the object cache so it will hit the data source ok($obj, 'Got object with id 5'); is($obj->name, 'EEE', 'It has the right name'); is($file_new, 1, 'new() was called on the file handle'); is($file_open, 1, 'open() was called on the file handle'); is($file_close, 1, 'close() was called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length*3, 'seek() was to the correct position'); # The uppercase iter gets us this far sub IO::File::Tracker::config_callbacks { my $class = shift; my %set_callbacks = @_; foreach my $key ( keys %set_callbacks) { $IO::File::Tracker::callbacks{$key} = $set_callbacks{$key}; } } sub IO::File::Tracker::_call_cb { my($op, @args) = @_; my $cb = $IO::File::Tracker::callbacks{$op}; if ($cb) { $cb->(@args); } } BEGIN { @IO::File::Tracker::ISA = qw( IO::File ); # Create overridden methods for the ones we want to track foreach my $subname (qw( new open close DESTROY seek tell getline ) ) { no strict 'refs'; my $subref = sub { my $self = shift; IO::File::Tracker::_call_cb($subname, @_); my $super = IO::File->can($subname); return $super->($self, @_); }; my $fq_subname = 'IO::File::Tracker::'.$subname; *$fq_subname = $subref; } } 04f_filemux.t100664023532023421 1101412544604516 15341 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 37; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeFileMux; &setup_files_and_classes(); my $obj = URT::Thing->get(thing_id => 1, thing_type => 'person'); ok($obj, 'Got a person thing with id 1'); is($obj->thing_name, 'Joel', 'Name is correct'); is($obj->thing_color, 'grey', 'Color is correct'); is($obj->thing_type, 'person', 'type is correct'); $obj = URT::Thing->get(thing_id => 6, thing_type => 'robot'); ok($obj, 'Got a robot thing with id 5'); is($obj->thing_name, 'Tom', 'Name is correct'); is($obj->thing_color, 'red', 'Color is correct'); $obj = URT::Thing->get(thing_id => 3, thing_type => 'person'); ok(!$obj, 'Correctly found no person thing with id 3'); my @objs = URT::Thing->get(thing_type => ['person','robot'], thing_id => 7); is(scalar(@objs),1, 'retrieved a thing with id 7 that is either a person or robot'); is($objs[0]->thing_id, 7, 'The retrieved thing has the right id'); is($objs[0]->thing_type, 'robot', 'The retrieved thing is a robot'); is($objs[0]->thing_name, 'Gypsy', 'Name is correct'); is($objs[0]->thing_color, 'purple', 'Color is correct'); my $filemux_error_message; URT::DataSource::SomeFileMux->error_messages_callback(sub { $filemux_error_message = $_[1]; $_[1] = undef }); $obj = eval { URT::Thing->get(thing_id => 2) }; ok(!$obj, "Correctly couldn't retrieve a Thing without a thing_type"); like($filemux_error_message, qr(Recursive entry.*URT::Thing), 'Error message did mention recursive call trapped'); my $iter = URT::Thing->create_iterator(thing_type => ['person', 'robot']); ok($iter, 'Created an iterator for all Things'); my $expected_id = 1; while (my $obj = $iter->next()) { ok($obj, 'Got an object from the iterator'); is($obj->id, $expected_id++, 'Its ID was the expected value'); } # Try the object pruner to unload the File data sources my @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 2, 'Two file data sources were defined'); @file_data_sources = (); my @warnings; { my @warnings = (); local $SIG{'__WARN__'} = sub { push @warnings, @_ }; UR::Context->object_cache_size_lowwater(1); UR::Context->object_cache_size_highwater(2); ok(UR::Context->current->prune_object_cache(), 'Force object cache pruning'); } @warnings = grep { $_ !~ m/After seceral passes of pruning the object cache, there are still \d+ objects/ } @warnings; is(scalar(@warnings), 0, 'No unexpected warnings from pruning'); UR::Context->object_cache_size_lowwater(undef); UR::Context->object_cache_size_highwater(undef); @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 0, 'After cache pruning, no file data sources are defined'); if (@file_data_sources) { foreach (@file_data_sources) { print STDERR Data::Dumper::Dumper($_); } } # try getting something again, should re-create the data source object $obj = UR::Context->current->reload('URT::Thing', thing_type => 'person', thing_id => 1); ok($obj, 'Reloading URT::Thing id 3'); @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 1, 'The File data source was re-created'); sub setup_files_and_classes { my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $delimiter = URT::DataSource::SomeFileMux->delimiter; my $file = "$dir/person"; my $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(1 Joel grey)),"\n"); $f->print(join($delimiter, qw(2 Mike blue)),"\n"); $f->print(join($delimiter, qw(4 Frank black)),"\n"); $f->print(join($delimiter, qw(5 Clayton green)),"\n"); $f->close(); $file = "$dir/robot"; $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(3 Crow gold)),"\n"); $f->print(join($delimiter, qw(6 Tom red)),"\n"); $f->print(join($delimiter, qw(7 Gypsy purple)),"\n"); $f->close(); my $c = UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, thing_type => { is => 'String', valid_values => ['person', 'robot'] }, ], table_name => 'wefwef', data_source => 'URT::DataSource::SomeFileMux', ); ok($c, 'Created class'); } 04f_filemux_sync_database.t100664023532023421 1102612544604516 20224 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 36; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeFileMux; my $ds = URT::DataSource::SomeFileMux->get(); ok($ds, 'got the datasource object'); &setup_files_and_classes($ds); my $thing1 = URT::Thing->get(thing_id => 1, thing_type => 'person'); ok($thing1, 'got an object'); ok($thing1->thing_color('changed'), 'Changed its color'); my $thing2 = URT::Thing->get(thing_id => 10, thing_type => 'robot'); ok($thing2, 'Got another object'); ok($thing2->thing_name('TomTom'), 'Changed its name'); my $thing3 = URT::Thing->get(thing_id => 2, thing_type => 'person'); ok($thing3, 'Got a third thing'); ok($thing3->delete, 'Deleted it'); my $new1 = URT::Thing->create(thing_id => 3, thing_name => 'Shaggy', thing_color => 'green', thing_type => 'person'); ok($new1, 'Created a new thing'); my $new2 = URT::Thing->create(thing_id => 9, thing_name => 'Fred', thing_color => 'white', thing_type => 'person'); ok($new2, 'Created a new thing 2'); my $new3 = URT::Thing->create(thing_id => 0, thing_name => 'Velma', thing_color => 'red', thing_type => 'person'); ok($new3, 'Created a new thing 3'); my $new4 = URT::Thing->create(thing_id => 11, thing_name => 'Robbie', thing_color => 'black', thing_type => 'robot'); ok($new4, 'Created a new thing 4'); my $new5 = URT::Thing->create(thing_id => 20, thing_name => 'Scooby', thing_color => 'brown', thing_type => 'animal'); ok($new5, 'Created a new thing 5'); ok(UR::Context->commit(), 'Commit'); &check_files($ds); foreach my $obj ( $new1, $new2, $new3, $new4, $new5 ) { ok(exists($obj->{'db_committed'}), "New object now has a 'db_committed' hash key") } sub check_files { my $ds = shift; my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $f = IO::File->new("$dir/person"); ok($f, 'Opened file for person data'); my $line = $f->getline(); is($line, qq(0\tVelma\tred\n), 'Line 0'); $line = $f->getline(); is($line, qq(1\tJoel\tchanged\n), 'Line 1'); $line = $f->getline(); is($line, qq(3\tShaggy\tgreen\n), 'Line 2'); $line = $f->getline(); is($line, qq(4\tFrank\tblack\n), 'Line 3'); $line = $f->getline(); is($line, qq(5\tClayton\tgreen\n), 'Line 4'); $line = $f->getline(); is($line, qq(9\tFred\twhite\n), 'Line 5'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); $f = IO::File->new("$dir/robot"); ok($f, 'Opened file for robot data'); $line = $f->getline(); is($line, qq(8\tCrow\tgold\n), 'Line 0'); $line = $f->getline(); is($line, qq(10\tTomTom\tred\n), 'Line 1'); $line = $f->getline(); is($line, qq(11\tRobbie\tblack\n), 'Line 3'); $line = $f->getline(); is($line, qq(12\tGypsy\tpurple\n), 'Line 2'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); $f = IO::File->new("$dir/animal"); ok($f, 'Opened file for animal data'); $line = $f->getline(); is($line, qq(20\tScooby\tbrown\n), 'Line 0'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); unlink("$dir/person", "$dir/robot", "$dir/animal"); } sub setup_files_and_classes { my $ds = shift; my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $delimiter = $ds->delimiter; unlink("$dir/person", "$dir/robot", "$dir/animal"); my $file = "$dir/person"; my $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(1 Joel grey)),"\n"); $f->print(join($delimiter, qw(2 Mike blue)),"\n"); $f->print(join($delimiter, qw(4 Frank black)),"\n"); $f->print(join($delimiter, qw(5 Clayton green)),"\n"); $f->close(); $file = "$dir/robot"; $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(8 Crow gold)),"\n"); $f->print(join($delimiter, qw(10 Tom red)),"\n"); $f->print(join($delimiter, qw(12 Gypsy purple)),"\n"); $f->close(); my $c = UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, thing_type => { is => 'String' }, ], table_name => 'wefwef', data_source => 'URT::DataSource::SomeFileMux', ); ok($c, 'Created class'); } 04g_rdbms_shared_table_name.t100664023532023421 415112544604516 20461 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; use URT::DataSource::SomeSQLite; use File::Temp qw(); my $new_ds_file = generate_somesqlite_datasource('URT::DataSource::AnotherSQLite'); require $new_ds_file->filename; my $table_name = 'thing'; my %class_of_ds = ( 'URT::DataSource::SomeSQLite' => 'SomeThing', 'URT::DataSource::AnotherSQLite' => 'AnotherThing', ); for my $ds (qw(URT::DataSource::SomeSQLite URT::DataSource::AnotherSQLite)) { my $dbh = $ds->get_default_handle(); my $sql = qq(create table $table_name (id integer)); ok($dbh->do($sql), "$ds: $sql"); UR::Object::Type->define( class_name => $class_of_ds{$ds}, id_by => 'id', data_source => $ds, table_name => $table_name, ); } my @classes_for_table = grep { $_->class_name !~ /::Ghost$/ } UR::Object::Type->is_loaded(table_name => $table_name); is(scalar(@classes_for_table), 2, 'got two classes for table'); for my $ds (qw(URT::DataSource::SomeSQLite URT::DataSource::AnotherSQLite)) { my $class_name = $ds->_lookup_class_for_table_name($table_name); is($class_name, $class_of_ds{$ds}, qq(class for '$table_name' on $ds is correct)); } for my $class (values %class_of_ds) { $class->create(id => 1) or die; } UR::Context->commit; for my $ds (qw(URT::DataSource::SomeSQLite URT::DataSource::AnotherSQLite)) { my $dbh = $ds->get_default_handle(); my $sth = $dbh->prepare(qq(select * from $table_name)); $sth->execute(); my $r = $sth->fetchall_arrayref(); is_deeply($r, [[1]], qq($ds: got expected row)); } sub generate_somesqlite_datasource { my $ds_class_name = shift; my $orig_path = $INC{'URT/DataSource/SomeSQLite.pm'}; my $orig_file = IO::File->new($orig_path, 'r'); local ($/); my $orig_source = <$orig_file>; $orig_source =~ s/URT::DataSource::SomeSQLite/$ds_class_name/g; my $new_file = File::Temp->new(TMPDIR => 1); $new_file->print($orig_source); $new_file->close(); return $new_file; } 04h_default_datasource.t100664023532023421 1523512544604516 17541 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 5; use Test::Exception; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace subtest 'load iterator'=> sub { plan tests => 3; class URT::DefaultLoaderIter { has => [qw/nose tail/], data_source => 'UR::DataSource::Default', }; sub URT::DefaultLoaderIter::__load__ { my ($class, $bx, $headers) = @_; # for testing purposes we ignore the $bx and $headers, # and return a 2-row, 3-column data set $headers = ['nose','tail','id']; my $body = [ ['wet','waggly', 1001], ['dry','perky', 1002], ]; my $iterator = sub { shift @$body }; return $headers, $iterator; } my $new = URT::DefaultLoaderIter->create(nose => 'long', tail => 'floppy', id => 1003); ok($new, 'made a new object'); # The system will trust the db engine, but then will merge results with any objects # already in memory. This means our new object matches, and even though only one # of the database rows match, the broken db above will return 2 more items. Totalling 3. my @p1 = URT::DefaultLoaderIter->get(nose => ['long','wet']); is(scalar(@p1), 2, "got two objects as expected, because we re-check the query engine by default"); # Now that the query results are cached, the bug in the db logic is hidden, and we return # the full results. my @p2 = URT::DefaultLoaderIter->get(nose => ['long','wet']); is(scalar(@p2), 2, "got two objects as expected"); }; subtest 'load list' => sub { plan tests => 2; class URT::DefaultLoadList { has => [qw/nose tail/], data_source => 'UR::DataSource::Default', }; sub URT::DefaultLoadList::__load__ { my ($class, $bx, $headers) = @_; # Same as the iter loader, but return a list of lists # representing the resultset $headers = ['nose','tail','id']; my $body = [ ['wet','waggly', 1001], ['dry','perky', 1002], ]; return $headers, $body; } # The system will trust the db engine, but then will merge results with any objects # already in memory. This means our new object matches, and even though only one # of the database rows match, the broken db above will return 2 more items. Totalling 3. my @p1 = URT::DefaultLoaderIter->get(nose => ['long','wet']); is(scalar(@p1), 2, "got two objects as expected, because we re-check the query engine by default"); # Now that the query results are cached, the bug in the db logic is hidden, and we return # the full results. my @p2 = URT::DefaultLoaderIter->get(nose => ['long','wet']); is(scalar(@p2), 2, "got two objects as expected"); }; subtest 'save' => sub { plan tests => 5; class URT::DefaultSave { has => [qw/nose tail/], data_source => 'UR::DataSource::Default', }; my @saved_ids; *URT::DefaultSave::__save__ = sub { my $self = shift; push @saved_ids, $self->id; }; my @committed_ids; *URT::DefaultSave::__commit__ = sub { my $self = shift; push @committed_ids, $self->id; }; # fake loading objects from the data source by defining them my $unchanged = URT::DefaultSave->__define__(id => 1, nose => 'black', tail => 'fluffy'); my $will_change = URT::DefaultSave->__define__(id => 2, nose => 'short', tail => 'blue'); # Make some changes ok($will_change->tail('black'), 'change existing object'); my $new_obj = URT::DefaultSave->create(id => 3, nose => 'medium', tail => 'smooth'); ok($new_obj, 'created new object'); ok(UR::Context->current->commit, 'commit changes'); is_deeply([ sort @saved_ids ], [2, 3], 'Proper objects were saved'); is_deeply([ sort @committed_ids ], [2, 3], 'Proper objects were committed'); }; subtest 'failure syncing' => sub { plan tests => 3; class URT::FailSync { data_source => 'UR::DataSource::Default', }; do { local *URT::FailSync::__save__ = sub { die "failed during save"; }; my $should_fail_during_rollback = 0; local *URT::FailSync::__rollback__= sub { die "failed during rollback" if $should_fail_during_rollback; }; my $obj = URT::FailSync->create(id => 1); throws_ok { UR::Context->current->commit() } qr/failed during save/, 'Failed in commit'; my $error_message_during_commit; UR::DataSource::Default->dump_error_messages(0); UR::DataSource::Default->add_observer( aspect => 'error_message', once => 1, callback => sub { my($self, $aspect, $message) = @_; $error_message_during_commit = $message; }, ); $should_fail_during_rollback = 1; throws_ok { UR::Context->current->commit() } qr/Failed to save, and ERRORS DURING ROLLBACK:\s+failed during save.*failed during rollback/s, 'Failed in commit second time'; like($error_message_during_commit, qr/Rollback failed:.*'id' => 1/s, 'error_message() mentions the object failed rollback'); }; UR::Context->current->rollback; # throw away errored objects }; subtest 'sync all before committing' => sub { plan tests => 4; class URT::SyncThenCommit { data_source => 'UR::DataSource::Default', }; my @objs = map { URT::SyncThenCommit->create(id => $_) } qw(1 2 3); my @synced_ids; my @committed_ids; *URT::SyncThenCommit::__save__ = sub { my $obj = shift; push @synced_ids, $obj->id; if (@committed_ids) { ok(0, 'Some objects were committed before all were synced') or diag explain @committed_ids; } }; *URT::SyncThenCommit::__commit__ = sub { my $obj = shift; push @committed_ids, $obj->id; }; UR::Context->current->add_observer( aspect => 'sync_databases', once => 1, callback => sub { is_deeply([ sort @synced_ids ], [ qw(1 2 3) ], 'Synced all objects'); is(scalar(@committed_ids), 0, 'No objects are committed yet'); }, ); UR::Context->current->add_observer( aspect => 'commit', once => 1, callback => sub { is_deeply([ sort @committed_ids ], [ qw(1 2 3) ], 'Committed all objects'); }, ); ok(UR::Context->current->commit, 'commit'); }; 05_get_create_get.t100664023532023421 541512544604516 16454 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 18; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; &create_tables_and_classes(); my $p1 = URT::Product->get(1); ok(!$p1, 'Get by non-existent ID correctly returns nothing'); my $p2 = URT::Product->create(id => 1, name => 'jet pack', genius => 6, manufacturer_name => 'Lockheed Martin',sc => 'URT::TheSubclass'); ok($p2, 'Create a new Product with the same ID'); $p1 = URT::Product->get(1); ok($p1, 'Get with the same ID returns something, now'); is($p1->id, 1, 'ID is correct'); is($p1->name, 'jet pack', 'name is correct'); is($p1->genius, 6, 'name is correct'); is($p1->manufacturer_name, 'Lockheed Martin', 'name is correct'); my @prods = URT::Product->get('genius between' => [1,10]); is(scalar(@prods), 1, 'get() with between works'); my $composite_id = join("\t", 1, 2); my $m = URT::MultiIdThing->get($composite_id); ok($m, 'Got MultiIdThing by composite ID'); is($m->id1, 1, 'id1 value'); is($m->id2, 2, 'id2 value'); is($m->value, 'test value', 'value value'); sub create_tables_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PRODUCT ( prod_id int NOT NULL PRIMARY KEY, name varchar, genius integer, manufacturer_name varchar, sc varchar)'), 'created product table'); ok($dbh->do('create table MULTI_ID_THING ( id1 int NOT NULL, id2 int NOT NULL, value varchar, PRIMARY KEY (id1, id2))'), 'created multi id thing table'); $dbh->do(q(insert into MULTI_ID_THING values (1, 2, 'test value'))); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', is_abstract => 1, id_by => [ prod_id => { is => 'NUMBER' }, ], has => [ name => { is => 'STRING' }, genius => { is => 'NUMBER' }, manufacturer_name => { is => 'STRING' }, sc => { is => 'String' }, ], subclassify_by => 'sc', data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); ok(UR::Object::Type->define( class_name => 'URT::TheSubclass', is => 'URT::Product', ), "Created class for TheSubclass"); ok(UR::Object::Type->define( class_name => 'URT::MultiIdThing', table_name => 'MULTI_ID_THING', id_by => ['id1','id2'], has => ['value'], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for MultiIdThing'); } 06_accessor_simple.t100664023532023421 133312544604516 16662 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my ($obj,$same_obj); use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name/] ); $obj = Acme::Product->create(name => "dynamite", manufacturer_name => "Explosives R US"); ok($obj, 'Created object with name and manufacturer_name'); is($obj->name, "dynamite", 'name accessor works'); is($obj->manufacturer_name, "Explosives R US", 'manufacturer_name accessor works'); # $same_obj = Acme::Product->get(name => "dynamite"); is($obj,$same_obj, 'Get same object returns the same reference'); 07_create_get_simple.t100664023532023421 264212544604516 17167 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my ($p1,$p2,$p3,$p4,$p5,$p6,$p7,@obj,@got,@expected); use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name/] ); $p1 = Acme::Product->create(name => "jet pack", manufacturer_name => "Lockheed Martin"); ok($p1, 'Created a jet pack'); $p2 = Acme::Product->create(name => "hang glider", manufacturer_name => "Boeing"); ok($p2, 'Created a hang glider'); $p3 = Acme::Product->create(name => "mini copter", manufacturer_name => "Boeing"); ok($p2, 'Created a mini copter'); $p4 = Acme::Product->create(name => "firecracker", manufacturer_name => "Explosives R US"); ok($p2, 'Created a firecracker'); $p5 = Acme::Product->create(name => "dynamite", manufacturer_name => "Explosives R US"); ok($p2, 'Created a dynamite'); $p6 = Acme::Product->create(name => "plastique", manufacturer_name => "Explosives R US"); ok($p2, 'Created a plastique'); @obj = Acme::Product->get(manufacturer_name => "Boeing"); is(scalar(@obj), 2, 'Two objects have manufacturer_name => "Boeing"'); # @obj = Acme::Product->get(); is(scalar(@obj), 6, 'There were six objects total'); @got = sort @obj; @expected = sort ($p1,$p2,$p3,$p4,$p5,$p6); is_deeply(\@got,\@expected, 'They are in the expected order'); 08_create_get_operators.t100664023532023421 1537212544604516 17741 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 570; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my @obj; use UR; use URT; # Singleton classes for testing isa operator UR::Object::Type->define( class_name => 'Acme::Status', is => 'UR::Singleton', ); UR::Object::Type->define( class_name => 'Acme::Status::Design', is => 'Acme::Status', ); UR::Object::Type->define( class_name => 'Acme::Status::Production', is => 'Acme::Status' ); # memory-only class UR::Object::Type->define( class_name => 'Acme::Product', has => ['name', 'manufacturer_name', 'genius', 'status', status_obj => { is => 'Acme::Status', id_by => 'status', id_class_by => 'status' }, ], ); # same properties, but in the DB my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table product (product_id integer NOT NULL PRIMARY KEY, name varchar, genius integer, manufacturer_name varchar, status varchar)') or die "Can't create product table"; UR::Object::Type->define( class_name => 'Acme::DBProduct', id_by => 'product_id', has => ['name', 'manufacturer_name', 'genius', 'status', status_obj => { is => 'Acme::Status', id_by => 'status', id_class_by => 'status' }, ], table_name => 'product', data_source => 'URT::DataSource::SomeSQLite', ); my @products = ( [ name => "jet pack", genius => 6, manufacturer_name => "Lockheed Martin", status => 'Acme::Status::Design' ], [ name => "hang glider", genius => 4, manufacturer_name => "Boeing", status => 'Acme::Status::Production'], [ name => "mini copter", genius => 5, manufacturer_name => "Boeing", status => 'Acme::Status::Production'], [ name => "catapult", genius => 5, manufacturer_name => "Boeing", status => 'Acme::Status::Design'], [ name => "firecracker", genius => 6, manufacturer_name => "Explosives R US", status => 'Acme::Status::Production'], [ name => "dynamite", genius => 9, manufacturer_name => "Explosives R US", status => 'Acme::Status::Production'], [ name => "plastique", genius => 8, manufacturer_name => "Explosives R US", status => 'Acme::Status::Design'], ); my $insert = $dbh->prepare('insert into product values (?,?,?,?,?)'); my $id = 1; foreach ( @products ) { Acme::Product->create(@$_); $insert->execute($id++, @$_[1,3,5, 7]) } $insert->finish(); $dbh->commit(); my @tests = ( # get params # num expected objects [ [ manufacturer_name => 'Boeing', genius => 5], 2 ], [ [ name => ['jet pack', 'dynamite'] ], 2 ], [ [ manufacturer_name => ['Boeing','Lockheed Martin'] ], 4 ], [ [ 'genius !=' => 9 ], 6 ], [ [ 'genius not' => 9 ], 6 ], [ [ 'genius not =' => 9 ], 6 ], [ [ 'manufacturer_name !=' => 'Explosives R US' ], 4 ], [ [ 'manufacturer_name like' => '%arti%' ], 1 ], [ [ 'manufacturer_name not like' => '%arti%' ], 6 ], [ [ 'genius <' => 6 ], 3 ], [ [ 'genius !<' => 6 ], 4 ], [ [ 'genius not <' => 6 ], 4 ], [ [ 'genius <=' => 6 ], 5 ], [ [ 'genius !<=' => 6 ], 2 ], [ [ 'genius not <=' => 6 ], 2 ], [ [ 'genius >' => 6 ], 2 ], [ [ 'genius !>' => 6 ], 5 ], [ [ 'genius not >' => 6 ], 5 ], [ [ 'genius >=' => 6 ], 4 ], [ [ 'genius !>=' => 6 ], 3 ], [ [ 'genius not >=' => 6 ], 3 ], [ [ 'genius between' => [4,6] ], 5 ], [ [ 'genius !between' => [4,6] ], 2 ], [ [ 'genius not between' => [4,6] ], 2 ], [ [ 'genius >' => 5, 'status isa' => 'Acme::Status::Production' ], 2 ], [ [ 'status isa' => 'Acme::Status::Design'], 3 ], [ [ 'status isa' => 'Acme::Status' ], 7 ], [ [ 'manufacturer_name >' => 'E' ], 4 ], [ [ 'manufacturer_name not >' => 'E' ], 3 ], [ [ 'manufacturer_name <' => 'E' ], 3 ], [ [ 'manufacturer_name not <' => 'E' ], 4 ], [ [ 'manufacturer_name >=' => 'E' ], 4 ], [ [ 'manufacturer_name not >=' => 'E' ], 3 ], [ [ 'manufacturer_name <=' => 'E' ], 3 ], [ [ 'manufacturer_name not <=' => 'E' ], 4 ], [ [ 'manufacturer_name between' => ['C', 'H'] ], 3 ], [ [ 'manufacturer_name not between' => ['C', 'H'] ], 4 ], ); for my $class ( qw( Acme::Product Acme::DBProduct ) ) { # Test with get() for (my $testnum = 0; $testnum < @tests; $testnum++) { my $params = $tests[$testnum]->[0]; my $expected = $tests[$testnum]->[1]; my @objs = $class->get(@$params); is(scalar(@objs), $expected, "Got $expected objects for $class->get() test $testnum: ".join(' ', @$params)); } # Test old syntax for (my $testnum = 0; $testnum < @tests; $testnum++) { my $params = $tests[$testnum]->[0]; my $expected = $tests[$testnum]->[1]; my %params; for(my $i = 0; $i < @$params; $i += 2) { my($prop, undef, $op) = $params->[$i] =~ m/^(\w+)(\s+(.*))?/; $params{$prop} = { operator => $op, value => $params->[$i+1] }; } my @objs = $class->get(%params); is(scalar(@objs), $expected, "Got $expected objects for $class->get() old syntax test $testnum: ".join(' ', @$params)); } # test get with a bx for (my $testnum = 0; $testnum < @tests; $testnum++) { my $params = $tests[$testnum]->[0]; my $expected = $tests[$testnum]->[1]; my $bx = $class->define_boolexpr(@$params); my @objs = $class->get($bx); is(scalar(@objs), $expected, "Got $expected objects for bx test $testnum: ".join(' ', @$params)); # test each param in the BX my %params = @$params; foreach my $key ( keys %params ) { ($key) = $key =~ m/(\w+)/; ok($bx->specifies_value_for($key), "bx does specify value for $key"); } foreach my $obj ( @objs) { ok($bx->evaluate($obj), 'Expected $obj '.$obj->id.' object passes the BoolExpr'); } } } 10_accessor_object.t100664023532023421 465712544604516 16646 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 7; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; UR::Object::Type->define( class_name => 'Acme', is => 'UR::Namespace' ); UR::Object::Type->define( class_name => 'Acme::Manufacturer', id_by => ['name'], has => [qw/name industry/], ); my $m1 = Acme::Manufacturer->create(name => "Lockheed Martin"); my $m2 = Acme::Manufacturer->create(name => "Boeing"); my $m3 = Acme::Manufacturer->create(name => "Explosives R US"); UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name' => {}, 'manufacturer' => { type => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius' => {}, #'manufacturer_name' => { via => 'manufacturer', to => 'name' }, ] ); my $p1 = Acme::Product->create(name => "jet pack", genius => 6, manufacturer => $m1); my $p2 = Acme::Product->create(name => "hang glider", genius => 4, manufacturer => $m2); Acme::Product->create(name => "mini copter", genius => 5, manufacturer => $m2); Acme::Product->create(name => "firecracker", genius => 6, manufacturer => $m3); Acme::Product->create(name => "dynamite", genius => 7, manufacturer => $m3); Acme::Product->create(name => "plastique", genius => 8, manufacturer => $m3); my @obj = Acme::Product->get(); is(scalar(@obj), 6, "got the expected objects"); #ok(Acme::Product->can("manufacturer"), "the object-accessor is present"); is(Acme::Product->get(name => "jet pack")->manufacturer->name, "Lockheed Martin", "object accessor works"); is(Acme::Product->get(name => "dynamite")->manufacturer->name, "Explosives R US", "object accessor works"); my $jetpack = Acme::Product->get(name => "jet pack"); ok($jetpack->manufacturer($m2), 'Change manufacturer on jet pack'); is($jetpack->manufacturer->name, 'Boeing', 'Change was successful'); eval { $jetpack->manufacturer('Boeing') }; ok($@, 'Setting the object accessor to a string throws an exception'); like($@, qr(Can't call method "id" without a package or object reference. Expected an object as parameter to 'manufacturer', not the value 'Boeing'), 'The exception was correct'); #is(Acme::Product->get(name => "jet pack")->manufacturer_name, "Lockheed Martin", "delegated accessor works"); #is(Acme::Product->get(name => "dynamite")->manufacturer_name, "Explosives R US", "delegated accessor works"); 11_create_with_delegated_property.t100664023532023421 310512544604516 21742 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More 'no_plan'; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; UR::Object::Type->define( class_name => 'Acme::Manufacturer', is => 'UR::Object', has => [qw/name/], ); UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name', 'manufacturer' => { is => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius' ] ); my $m1 = Acme::Manufacturer->create(name => "Lockheed Martin"); my $m2 = Acme::Manufacturer->create(name => "Boeing"); my $m3 = Acme::Manufacturer->create(name => "Explosives R US"); my $p = Acme::Product->create(name => "jet pack", genius => 6, manufacturer => $m1); ok($p, "created a product"); is($p->manufacturer_id,$m1->id,"manufacturer on product is correct"); is($p->manufacturer,$m1,"manufacturer on product is correct"); __END__ Acme::Product->create(name => "hang glider", genius => 4, manufacturer => $m2); Acme::Product->create(name => "mini copter", genius => 5, manufacturer => $m2); Acme::Product->create(name => "firecracker", genius => 6, manufacturer => $m3); Acme::Product->create(name => "dynamite", genius => 7, manufacturer => $m3); Acme::Product->create(name => "plastique", genius => 8, manufacturer => $m3); print Data::Dumper::Dumper(Acme::Product->get(name => 'jet pack')); exit; is(Acme::Product->get(name => "jet pack")->manufacturer->name, "Lockheed Martin"); is(Acme::Product->get(name => "dynamite")->manufacturer->name, "Explosives R US"); 11b_via_to_without_type.t100664023532023421 171712544604516 17760 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; my $c1 = UR::Object::Type->define( class_name => 'Acme::Manufacturer', is => 'UR::Object', has => [ name => { is => 'Text' }, ], ); my $c2 = UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name', 'manufacturer' => { is => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius', 'manufacturer_name' => { via => 'manufacturer', to => 'name' }, ] ); my $p2 = $c2->property('manufacturer_name'); ok($p2, "got property meta for a via/to with undeclared type"); # we currently leave the data_type un-set # is($p2->data_type, "Text", "data type is set to the correct value"); is($p2->_data_type_as_class_name, "UR::Value::Text", "class for the data type is set to the correct value"); 11c_create_with_via_property.t100664023532023421 510712544604516 20752 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 20; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; UR::Object::Type->define( class_name => 'URT::Office', id_by => 'office_id', has => ['office_number'], ); UR::Object::Type->define( class_name => 'URT::Boss', id_by => 'boss_id', has => [ name => { is => 'Text' }, office => { is => 'URT::Office', id_by => 'office_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ name => { is => 'Text' }, boss => { is => 'URT::Boss', id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'name' }, boss_office => { is => 'URT::Office', via => 'boss', to => 'office' }, boss_office_number => { via => 'boss_office', to => 'office_number' }, ], ); my $o = URT::Office->create(office_number => 123); ok($o, 'Created office 123'); my $b = URT::Boss->create(name => 'Montgomery', office => $o); ok($b, 'Created boss with an office'); is($b->office_id, $o->id, 'Boss office_id is correct'); is($b->office, $o, 'Boss office is correct'); my $e = URT::Employee->create(name => 'Homer', boss => $b); ok($e, 'Created an employee with a boss'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); my $bx = URT::Employee->define_boolexpr(name => 'Mindy', boss_name => 'Montgomery'); ok($bx, 'Created BoolExpr with an Employee name and boss_name'); $bx = URT::Employee->define_boolexpr(name => 'Mindy', boss_office => $o); ok($bx, 'Created BoolExpr with an Employee name and boss_office'); $e = URT::Employee->create(name => 'Lenny', boss_office => $o); ok($e, 'Created an employee with a boss_office'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); $e = URT::Employee->create(name => 'Carl', boss => $b, boss_office => $o); ok($e, 'Created an employee with a consistent boss and boss_office'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); my $o2 = URT::Office->create(office_number => 456); ok($o2, 'Created office 456'); $e = eval { URT::Employee->create(name => 'Frank', boss => $b, boss_office => $o2) }; ok(!$e, 'Correctly couldn not create an employee with conflicting boss and boss_office'); 11d_create_with_single_delegated_property_via_is_many_property.t100664023532023421 410512544604516 27772 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 7; # classes class Person::Relationship { is => 'UR::Object', id_by => [ person_id => { is => 'Number', implied_by => 'person', }, related_id => { is => 'Number', implied_by => 'related' }, name => { is => 'Text', }, ], has => [ person => { is => 'Person', id_by => 'person_id', }, related => { is => 'Person', id_by => 'related_id' }, ], }; class Person { is => 'UR::Object', has => [ name => { is => 'Text', doc => 'Name of the person', }, relationships => { is => 'Person::Relationship', is_many => 1, is_optional => 1, reverse_as => 'person', doc => 'This person\'s relationships', }, friends => { is => 'Person', is_many => 1, is_optional => 1, is_mutable => 1, via => 'relationships', to => 'related', where => [ name => 'friend' ], doc => 'Friends of this person', }, best_friend => { is => 'Person', is_optional => 1, is_mutable => 1, via => 'relationships', to => 'related', where => [ name => 'best friend' ], doc => 'Best friend of this person', }, ], }; my $ronnie = Person->create( name => 'Ronald Reagan', ); ok($ronnie, 'created Ronnie'); is_deeply([$ronnie->friends], [], 'Ronnie does not have friends'); ok(!$ronnie->best_friend, 'Ronnie does not have a best friend'); # Create George my $bill = Person->create( name => 'Bill Clinton', friends => [$ronnie], #works ); is_deeply([$bill->friends], [$ronnie], 'Bill has friend(s)'); my $george = Person->create( name => 'George HW Bush', friends => [$ronnie], best_friend => $bill, #does not work ); ok($george, 'created George'); is_deeply([$george->friends], [$ronnie], 'George has friend(s)'); is_deeply($george->best_friend, $bill, 'George is best friends w/ bill'); 11e_copy.t100664023532023421 460012544604516 14622 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 3; use Test::Deep qw(cmp_bag); use UR::Test qw(txtest); use UR; UR::Object::Type->define( class_name => 'Sports::Player', has => [ name => { is => 'Text' }, ], has_optional => [ team_id => { is => 'Text' }, team => { is => 'Sports::Team', id_by => 'team_id', }, nicknames => { is => 'Text', is_many => 1, }, ], ); UR::Object::Type->define( class_name => 'Sports::Team', has => [ name => { is => 'Text', }, ], has_optional => [ players => { is => 'Sports::Player', is_many => 1, reverse_as => 'team', }, ], ); txtest 'basic copy' => sub { plan tests => 3; my $lakers = Sports::Team->create(name => 'Lakers'); my $mj = Sports::Player->create(team_id => $lakers->id, name => 'Magic Johnson'); is_deeply([$lakers->players], [$mj], 'lakers have mj'); my $copied_team = $lakers->copy(); is_deeply([$copied_team->players], [], 'copied team has no players'); is($copied_team->name, $lakers->name, 'name was copied'); }; txtest 'basic copy with overrides' => sub { plan tests => 3; my $lakers = Sports::Team->create(name => 'Lakers'); my $mj = Sports::Player->create(team_id => $lakers->id, name => 'Magic Johnson'); is_deeply([$lakers->players], [$mj], 'lakers have mj'); my $copied_team = $lakers->copy(name => 'Clippers'); is_deeply([$copied_team->players], [], 'copied team has no players'); isnt($copied_team->name, $lakers->name, 'name was overrode'); }; txtest 'copy is_many properties' => sub { plan tests => 5; UR::Object::Type->define( class_name => 'Foo', has_many => [ things => { is => 'Text' } ], ); my $source = Foo->create(); $source->things([qw(one two)]); $source->add_thing('three'); $source->add_thing('four'); cmp_bag([$source->things], [qw(one two three four)]); my $copy = $source->copy(); is(ref($source->{things}), 'ARRAY', 'things has ARRAY reference type'); is(ref($copy->{things}), ref($source->{things}), 'things have same reference type'); isnt($copy->{things}, $source->{things}, 'copy did not reuse reference'); cmp_bag([$copy->things], [$source->things], 'copy has the same things'); }; 12_properties_metadata_query.t100664023532023421 311612544604516 20766 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; plan tests => 7; class X { has => [ x1 => { is => 'Text', doc => 'this is property x1 boo' }, x2 => { is => 'Text', doc => 'this is property x2' }, x3 => { is => 'Text', doc => 'this is property x3' }, x4 => { is => 'Text', doc => 'this is property x4' }, ], }; class Y { is => ['X'], has => [ y1 => { is => 'Text', doc => 'this is property y blah1' }, y2 => { is => 'Text', doc => 'this is property y2 boo' }, x1 => { doc => 'override of x1 in Y' }, x4 => { doc => 'override of x4 in Y' }, ], }; class Z { is => ['Y'], has => [ z1 => { is => 'Text', doc => 'this is property z1' }, z2 => { is => 'Text', doc => 'this is property z2 blah' }, y1 => { doc => 'override of y1 in Z' }, x3 => { doc => 'override of x1 in Z' }, x4 => { doc => 'override of x4 in Z which is also overriden in Y' }, ], }; my $m = Z->__meta__; ok($m, "got meta for class Z"); my @p; my $p; @p = $m->_properties(); is(scalar(@p), 9, "got 8 properties, as expected"); @p = $m->_properties("doc like" => '%x4%'); is(scalar(@p), 1, "got 1 x4 property"); $p = $p[0]; is($p->class_name, "Z", "class name is Z as expected"); is($p->property_name, "x4", "property name is x4 as expected"); $p = $m->property('x1'); ok($p, "got 1 x1 property"); is($p->property_name,"x1","property name is correct"); 13a_messaging.t100664023532023421 1440212544604517 15645 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use IO::Socket; use Data::Dumper; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use Test::More tests => 828; use UR::Namespace::Command::Old::DiffRewrite; my $c = "UR::Namespace::Command::Old::DiffRewrite"; # The messaging methods print to the filehandle $Command::stderr, which defaults # to STDERR. Redefine it so the messages are printed to a filehandle we # can read from, $stderr_twin, but regular perl diagnostic messages still go # to the real STDERR my $stderr_twin; $UR::ModuleBase::stderr = undef; socketpair($UR::ModuleBase::stderr,$stderr_twin, AF_UNIX, SOCK_STREAM, PF_UNSPEC); $UR::ModuleBase::stderr->autoflush(1); $stderr_twin->blocking(0); my $buffer; for my $type (qw/error warning status/) { my $accessor = $type . "_message"; my $uc_type = uc($type); my $msg_prefix = ($type eq "status" ? "" : "$uc_type: "); my $msg_source_sub = $accessor . '_source'; for my $do_queue ([],[0],[1]) { for my $do_dump ([],[0],[1]) { my $dump_flag = "dump_" . $type . "_messages"; $c->$dump_flag(@$do_dump); my $queue_flag = "queue_" . $type . "_messages"; $c->$queue_flag(@$do_queue); my $list_accessor = $accessor . "s"; is($c->$accessor(), undef , "$type starts unset"); $buffer = $stderr_twin->getline; is($buffer, undef, "no message"); my $cb_register = $type . "_messages_callback"; my $cb_msg_count = 0; my @cb_args; my $callback_sub = sub { @cb_args = @_; $cb_msg_count++;}; ok($c->$cb_register($callback_sub), "can set callback"); is($c->$cb_register(), $callback_sub, 'can get callback'); my $message_line = __LINE__ + 1; # The messaging sub will be called on the next line is($c->$accessor("error%d", 1), "error1", "$type setting works"); $buffer = $stderr_twin->getline; is($buffer, ($c->$dump_flag ? "${msg_prefix}error1\n" : undef), ($c->$dump_flag ? "got message 1" : "no dump") ); my %source_info = $c->$msg_source_sub(); is_deeply(\%source_info, { $accessor => 'error1', $type.'_package' => 'main', $type.'_file' => __FILE__, $type.'_line' => $message_line, $type.'_subroutine' => undef }, # not called from within a sub "$msg_source_sub returns correct info"); is($cb_msg_count, 1, "$type callback fired"); is_deeply( \@cb_args, [$c, "error1"], "$type callback got correct args" ); is($c->$accessor(), "error1", "$type returns"); $buffer = $stderr_twin->getline; is($buffer, undef, "no dump"); is($c->$accessor("error2"), "error2", "$type resetting works"); $buffer = $stderr_twin->getline; is($buffer, ($c->$dump_flag ? "${msg_prefix}error2\n" : undef), ($c->$dump_flag ? "got message 2" : "no dump") ); is($cb_msg_count, 2, "$type callback fired"); is($c->$accessor(), "error2", "$type returns"); is_deeply( \@cb_args, [$c, "error2"], "$type callback got correct args" ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); is($c->$accessor(undef), undef , "undef message sent to $type"); is($cb_msg_count, 3, "$type callback fired"); $buffer = $stderr_twin->getline; is($buffer, undef, 'Setting undef message results in no output'); is($c->$accessor(), undef , "$type still has the previous message"); is_deeply( \@cb_args, [$c, undef], "$type callback got correct args" ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); my $listref_accessor = $list_accessor . "_arrayref"; my $listref = $c->$listref_accessor(); is_deeply( $listref, ($c->$queue_flag ? ['error1','error2'] : []), "$type listref is correct" ); $c->$cb_register(sub { $_[1] .= "foo"}); $c->$accessor("altered"); $buffer = $stderr_twin->getline(); is($buffer, ($c->$dump_flag ? "${msg_prefix}alteredfoo\n" : undef), ($c->$dump_flag ? "got altered message" : "no dump") ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2","alteredfoo"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); $c->$cb_register(undef); # Unset the callback is($c->$accessor(undef), undef , "undef message sent to $type message"); is($cb_msg_count, 3, "$type callback correctly didn't get fired"); $buffer = $stderr_twin->getline(); is($buffer, undef, 'Setting undef message results in no output'); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2","alteredfoo"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); if ($c->$queue_flag) { $listref->[2] = "something else"; is_deeply( [$c->$list_accessor], ["error1","error2","something else"], "$type list is correct after changing via the listref" ); @$listref = (); is_deeply( [$c->$list_accessor], [], "$type list cleared out as expected" ); } } } } 1; 13b_dump_message_inheritance.t100664023532023421 2204412544604517 20714 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tBEGIN { # This environment variable overrides default UR behavior so unsetting it # for the test. delete $ENV{UR_COMMAND_DUMP_DEBUG_MESSAGES}; }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More; plan tests => 142; ok(UR::Object::Type->define( class_name => 'A'), 'Define class A'); ok(UR::Object::Type->define( class_name => 'B'), 'Define class B'); my $a = A->create(id => 1); ok($a, 'Create object a'); my $b = B->create(id => 1); ok($b, 'Create object b'); # Make sure each instance can control its own messaging flags is($a->dump_debug_messages(0), 0, 'Set dump_debug_messages on a to 0'); is($a->dump_debug_messages(), 0, 'dump_debug_messages on a is still 0'); is($b->dump_debug_messages(1), 1, 'Set dump_debug_messages on b to 1'); is($b->dump_debug_messages(), 1, 'dump_debug_messages on b is still 1'); is($a->dump_debug_messages(), 0, 'dump_debug_messages on a is still 0'); is($b->dump_debug_messages(), 1, 'dump_debug_messages on b is still 1'); # Make sure classes inherit their messaging behavior from parents if they don't # otherwise change it ok(UR::Object::Type->define( class_name => 'Parent'), 'Define class Parent'); ok(UR::Object::Type->define( class_name => 'ChildA', is => 'Parent'), 'Define class ChildA'); ok(UR::Object::Type->define( class_name => 'ChildB', is => 'Parent'), 'Define class ChildB'); $a = ChildA->create(); ok($a, 'Create object a'); $b = ChildB->create(); ok($b, 'Create object b'); is(Parent->dump_debug_messages(), undef, 'Parent dump_debug_messages() starts off as undef'); is(Parent->dump_debug_messages(0), 0, 'Setting Parent dump_debug_messages() to 0'); is(ChildA->dump_debug_messages(), 0, 'ChildA dump_debug_messages() is 0'); is($a->dump_debug_messages(), 0, 'object a dump_debug_messages() is 0'); is(ChildB->dump_debug_messages(), 0, 'ChildB dump_debug_messages() is 0'); is($b->dump_debug_messages(), 0, 'object b dump_debug_messages() is 0'); # Change the parent and the child classes and instances inherit it, since they haven't # overriden anything yet foreach my $val ( 1, 0 ) { is(Parent->dump_debug_messages($val), $val, "Change Parent dump_debug_messages() to $val"); is(Parent->dump_debug_messages(), $val, 'Parent dump_debug_messages() is set'); is(ChildA->dump_debug_messages(), $val, 'ChildA dump_debug_messages() matches Parent'); is($a->dump_debug_messages(), $val, 'object a dump_debug_messages() matches Parent'); is(ChildB->dump_debug_messages(), $val, 'ChildB dump_debug_messages() matches Parent'); is($b->dump_debug_messages(), $val, 'object b dump_debug_messages() matches Parent'); } # Twiddle both the parent and one of the child classes foreach my $parent_val ( 2, 1, 0) { is(Parent->dump_debug_messages($parent_val), $parent_val, "Set Parent dump_debug_messages() to $parent_val"); foreach my $child_val ( 1, 0 ) { is(ChildA->dump_debug_messages($child_val), $child_val, "Change ChildA dump_debug_messages() to $child_val"); is(ChildA->dump_debug_messages(), $child_val, 'ChildA dump_debug_messages() is set'); is($a->dump_debug_messages(), $child_val, 'object a dump_debug_messages() matches ChildA'); is(Parent->dump_debug_messages(), $parent_val, 'Parent dump_debug_messages() is still set'); is(ChildB->dump_debug_messages(), $parent_val, 'ChildB dump_debug_messages() matches Parent'); is($b->dump_debug_messages(), $parent_val, 'object b dump_debug_messages() matches Parent'); } } my $a2 = ChildA->create(); my $b2 = ChildB->create(); # Explicity set all the invilved entities is(Parent->dump_debug_messages(1), 1, ' Set Parent dump_debug_messages() to 1'); is(ChildA->dump_debug_messages(2), 2, ' Set ChildA dump_debug_messages() to 2'); is(ChildB->dump_debug_messages(3), 3, ' Set Parent dump_debug_messages() to 3'); is($a->dump_debug_messages(4), 4, ' Set object a dump_debug_messages() to 4'); is($a2->dump_debug_messages(5), 5, ' Set object a2 dump_debug_messages() to 5'); is($b->dump_debug_messages(6), 6, ' Set object b dump_debug_messages() to 6'); is($b2->dump_debug_messages(7), 7, ' Set object b dump_debug_messages() to 7'); # Check the values is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 2, 'ChildA dump_debug_messages() is 2'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 4, 'object a dump_debug_messages() is 4'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); # Now, start setting some of them to undef, meaning they should again inherit from their parent #diag('Clear setting on object a'); $a->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 2, 'ChildA dump_debug_messages() is 2'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 2, 'object a dump_debug_messages() is now 2'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on ChildA'); ChildA->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object a2'); $a2->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object b'); $b->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 3, 'object b dump_debug_messages() is 3'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on ChildB'); ChildB->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object b2'); $b2->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); my @ENV_VARS = ('UR_DUMP_DEBUG_MESSAGES', 'UR_COMMAND_DUMP_DEBUG_MESSAGES'); $DB::single=1; foreach $var ( @ENV_VARS ) { delete $ENV{$_} foreach @ENV_VARS; # clear them first #diag("use the $var env var"); $ENV{$var} = 99; is(Parent->dump_debug_messages(), 99, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 99, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 99, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 99, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 99, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 99, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 99, 'object b dump_debug_messages() is 1'); } 13c_message_observers.t100664023532023421 270612544604517 17374 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use strict; use warnings; use Test::More; plan tests => 8; ok(UR::Object::Type->define( class_name => 'Parent', is_abstract => 1), 'Define Parent class'); ok(UR::Object::Type->define( class_name => 'ChildA', is => 'Parent'), 'Define class ChildA'); ok(UR::Object::Type->define( class_name => 'ChildB', is => 'Parent'), 'Define class ChildB'); my $a = ChildA->create(id => 1); ok($a, 'Create object a'); my $b = ChildB->create(id => 1); ok($b, 'Create object b'); is(Parent->dump_status_messages(0), 0, 'Turn off dump_status_messages'); my %callbacks_fired; foreach my $class (qw( Parent ChildA ChildB )) { $class->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{$class}++; }); } $a->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{'objecta'}++; }); $b->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{'objectb'}++; }); ok($a->status_message('Hi'), 'sent status message to object a'); is_deeply(\%callbacks_fired, { Parent => 1, ChildA => 1, objecta => 1 }, 'Callbacks fired correctly'); 13d_command_debug.t100664023532023421 737212544604517 16447 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 5; use Sub::Install qw(install_sub); use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; subtest 'setup test' => sub { plan tests => 3; UR::Object::Type->define( class_name => 'SomeModule', ); ok(SomeModule->__meta__, 'defined SomeModule'); install_sub({ into => 'SomeModule', as => 'do_something', code => sub { my $self = shift; $self->debug_message('do_something'); }, }); for my $base_class ('Command::V1', 'Command::V2') { my $class_name = 'Some' . $base_class; UR::Object::Type->define( class_name => $class_name, is => $base_class, ); ok($class_name->__meta__, "defined $class_name"); install_sub({ into => $class_name, as => '_execute_body', code => sub { my $self = shift; $self->debug_message('execute'); my $sm = SomeModule->create(); $sm->do_something(); }, }); install_sub({ into => $class_name, as => 'debug', code => sub { my $self = shift; my $debug = $self->super_can('debug'); if ($self->$debug) { return IO::File->new('/dev/null', 'w'); } else { return 0; } }, }); } }; my $command_test_count = 6; my $command_test = sub { my $class = shift; my $debug = shift; my ($dump_debug_messages, $debug_message) = setup_subtest($class); is($dump_debug_messages->{'SomeModule'}, 0, 'dump_debug_messages disabled on SomeModule'); is($dump_debug_messages->{$class}, 0, "dump_debug_messages disabled on $class"); $class->_execute_delegate_class_with_params($class, {debug => $debug}); is($debug_message->{'SomeModule'}, 1, 'debug_message fired on SomeModule'); is($debug_message->{$class}, 1, "debug_message fired on $class"); my $status = $debug ? 'enabled' : 'disabled'; ok(!!$dump_debug_messages->{'SomeModule'} == !!$debug, "dump_debug_messages $status on SomeModule"); ok(!!$dump_debug_messages->{$class} == !!$debug, "dump_debug_messages $status on $class"); }; subtest 'Command::V1 with --debug' => sub { plan tests => $command_test_count; $command_test->('SomeCommand::V1', 1); }; subtest 'Command::V1 without --debug' => sub { plan tests => $command_test_count; $command_test->('SomeCommand::V1', 0); }; subtest 'Command::V2 with --debug' => sub { plan tests => $command_test_count; $command_test->('SomeCommand::V2', 1); }; subtest 'Command::V2 without --debug' => sub { plan tests => $command_test_count; $command_test->('SomeCommand::V2', 0); }; sub setup_subtest { my $cmd_class = shift; UR::ModuleBase->dump_debug_messages(0); $cmd_class->dump_debug_messages(0); my %dump_debug_messages = ( 'SomeModule' => SomeModule->dump_debug_messages, $cmd_class => $cmd_class->dump_debug_messages, ); my %debug_message = ( 'SomeModule' => 0, $cmd_class => 0, ); my $callback = sub { my ($self, $type, $message) = @_; my $class = $self->class; $dump_debug_messages{$class} = $self->dump_debug_messages; $debug_message{$class} = 1; }; SomeModule->add_observer( aspect => 'debug_message', callback => $callback, ); $cmd_class->add_observer( aspect => 'debug_message', callback => $callback, ); return (\%dump_debug_messages, \%debug_message); } 13e_messaging_format_string.t100664023532023421 277712544604517 20603 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More; my @types = qw/error warning status/; plan tests => (4 * @types); my $test_obj = UR::Value->create('test value'); for my $type (@types) { my $accessor = "${type}_message"; my $dump = "dump_${type}_messages"; $test_obj->$dump(0); my $return_val_with_format_string = $test_obj->$accessor('Hello, I like %s.', 'turkey sandwiches'); is($return_val_with_format_string, 'Hello, I like turkey sandwiches.', "When given multiple arguments, $type treats it like a format string"); my $val_with_invalid_format_string = 'Hello, this is not a valid format string %J'; my $return_val_without_format_string = $test_obj->$accessor($val_with_invalid_format_string); is($val_with_invalid_format_string, $return_val_without_format_string, "When given a single argument, $type does not run it through sprintf"); my $warn_msg; { local $SIG{__WARN__} = sub { $warn_msg = $_[0]; }; $test_obj->$accessor('%J', 'foo'); } like($warn_msg, qr/Invalid conversion in sprintf|Redundant argument in sprintf/, "When given an invalid format string, $type throws a warning"); my $file = __FILE__; like($warn_msg, qr/$file/, "When given an invalid format string, $type throws a warning from correct perspective"); } 14_ghost_objects.t100664023532023421 400512544604517 16343 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; use URT::DataSource::SomeSQLite; plan tests => 14; &setup_files_and_classes(); my $obj1 = URT::Things->get(thing_id => 1); ok($obj1, 'Loaded thing_id 1'); $obj2 = URT::Things::Ghost->get(thing_id => 2); ok (!$obj2, "Correctly couldn't load a ghost with thing_id 2"); ok($UR::Context::all_objects_loaded->{'URT::Things'}->{'1'}, 'thing_id 1 is in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things'}->{'2'}, 'thing_id 2 is not in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things::Ghost'}->{'1'}, 'thing_id 1 ghost is not in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things::Ghost'}->{'2'}, 'thing_id 2 ghost is not in the cache'); ok($obj1->delete(), 'thing_id 1 object deleted'); my $delobj = URT::Things->get(thing_id => 1); ok(! $delobj, 'thing_id 1 object no longer exists'); $delobj = URT::Things::Ghost->get(thing_id => 1); ok($delobj, 'thing_id 1 ghost object does exist'); 1; sub setup_files_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok( $dbh->do('create table things (thing_id integer, thing_value varchar)'), 'created table things'); ok($dbh->do("insert into things (thing_id, thing_value) values (1, 'foo')"), 'insert row 1 into things'); ok($dbh->do("insert into things (thing_id, thing_value) values (2, 'bar')"), 'insert row 2 into things'); ok($dbh->do("insert into things (thing_id, thing_value) values (3, 'foo')"), 'insert row 3 into things'); my $meta = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => [ thing_value => { is => 'String' }, ], table_name => 'THINGS', data_source => 'URT::DataSource::SomeSQLite', ); ok($meta, 'Created class for URT::Things'); } 15_singleton.t100664023532023421 731612544604517 15521 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 35; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $co = UR::Object::Type->define( class_name => 'URT::Parent', ); ok($co, 'Defined a parent, non-singleton class'); $co = UR::Object::Type->define( class_name => 'URT::SomeSingleton', is => ['UR::Singleton', 'URT::Parent'], has => [ property_a => { is => 'String' }, ], ); ok($co, 'Defined URT::SomeSingleton class'); $co = UR::Object::Type->define( class_name => 'URT::ChildSingleton', is => [ 'URT::SomeSingleton','UR::Singleton' ], has => [ property_b => { is => 'String' }, ], ); ok($co, 'Defined URT::ChildSingleton class'); $co = UR::Object::Type->define( class_name => 'URT::GrandChild', is => [ 'URT::ChildSingleton'], ); ok($co, 'Defined URT::GrandChild class'); ok(URT::GrandChild->create(id => 'URT::GrandChild', property_a => 'foo', property_b=>'bar'), 'Created a URT::GrandChild object'); my $obj = URT::SomeSingleton->_singleton_object(); ok($obj, 'Got the URT::SomeSingleton object through _singleton_object()'); isa_ok($obj, 'URT::SomeSingleton'); is($obj->property_a('hello'), 'hello', 'Setting property_a on URT::SomeSingleton object'); is($obj->property_a(), 'hello', 'Getting property_a on URT::SomeSingleton object'); is($obj->{property_a}, 'hello', 'Object key was filled in'); is(URT::SomeSingleton->property_a(), 'hello', "Getting property via singleton's class"); is(URT::SomeSingleton->property_a('bye'), 'bye', "Setting property_a on URT::SomeSingleton class"); is($obj->property_a(), 'bye', 'Getting property_a on URT::SomeSingleton object'); is($obj->{property_a}, 'bye', 'Object key was filled in'); is(URT::SomeSingleton->property_a(), 'bye', "Getting property via singleton's class"); my $obj2 = URT::SomeSingleton->get(); ok($obj2, 'Calling get() on URT::SomeSingleton returns an object'); is_deeply($obj,$obj2, 'The two objects are the same'); $obj = URT::ChildSingleton->_singleton_object(); ok($obj, 'Got the URT::ChildSingleton object through _singleton_object()'); isa_ok($obj, 'URT::ChildSingleton'); isa_ok($obj, 'URT::SomeSingleton'); is($obj->property_a('foo'), 'foo', 'Setting property_a on URT::ChildSingleton object'); is($obj->property_a(), 'foo', 'Getting property_a on URT::ChildSingleton object'); is($obj->property_b('blah'), 'blah', 'Setting property_b on URT::ChildSingleton object'); is($obj->property_b(), 'blah', 'Getting property_b on URT::ChildSingleton object'); $obj2 = URT::ChildSingleton->get(); ok($obj2, 'Calling get() on URT::ChildSingleton returns an object'); is_deeply($obj,$obj2, 'The two objects are the same'); my @objs = URT::Parent->get(); is(scalar(@objs), 3, 'get() via parent class returns 3 objects'); ok($obj->delete(), 'Delete the URT::ChildSingleton'); @objs = URT::Parent->get(); is(scalar(@objs), 2, 'get() via parent class returns 2 objects'); $co = UR::Object::Type->define( class_name => 'URT::ROSingleton', is => ['UR::Singleton'], has => [ property_a => { is => 'String', value => '123abc', is_mutable => 0 }, ], ); ok($co, 'Defined URT::ROSingleton class with read-only property'); $obj = URT::ROSingleton->_singleton_object; ok($obj, 'Get the URT::ROSingleton object through _singleton_object()'); is(URT::ROSingleton->property_a, '123abc', 'read-only property has current value as class method'); is($obj->property_a, '123abc', 'read-only property has current value as instance method'); ok(! eval {$obj->property_a('different') }, 'Setting a different value fails'); like($@, qr(Cannot change read-only property property_a for class URT::ROSingleton), 'exception is correct'); 16_viewer.t100664023532023421 352112544604517 15013 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More; eval { App::UI->user_interface("gtk2") }; if ($@) { plan skip_all => "skipping because gtk will not initialize", } else { plan tests => 7; #Gtk->timeout_add( if(0) { Glib::Timeout->add( 1000, # = 1 second ...slow me down for debugging sub { my @w = App::UI::Gtk2->windows(); for my $window (@w) { my $viewer_widget = $window->child(); $window->remove($viewer_widget); $window->destroy; App::UI::Gtk2->remove_window($window); } return 1; } ); } } App->init; my $v = URT::RAMThingy->create_viewer( toolkit => "gtk2", aspects => [ 'clone_name', 'chromosome', ], ); ok($v, "created a viewer"); $v->show_modal; my @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name chromosome/], "aspects are correct"); my $s = URT::RAMThingy->create(clone_name => "MY_CC-loneA01", map_order => 123, chromosome => "y"); ok($s, "created a subject"); $v->set_subject($s); is($v->get_subject,$s, "set the subject for the viewer"); $v->show_modal; ok($v->add_aspect("map_order"), "added a new aspect"); @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name chromosome map_order/], "returned aspects reflect the new addition"); $v->show_modal; $v->remove_aspect("chromosome"); @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name map_order/], "returned aspects reflect the removal"); $v->show_modal; 1; 17_accessor_object_basic.t100664023532023421 545112544604517 20010 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 31; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); UR::Object::Type->define( class_name => "Acme::Boss", has => [ id => { type => "Number" }, name => { type => "String" }, ] ); UR::Object::Type->define( class_name => 'Acme::Employee', has => [ name => { type => "String" }, #boss => { type => "Acme::Boss", id_by => [ boss_id => { type => "Number" } ] }, boss => { type => "Acme::Boss", id_by => 'boss_id' }, ] ); my $c = Acme::Employee->__meta__; my @p = sort $c->all_property_names; is_deeply(\@p, [qw/boss_id name/], "got expected old-style properties"); ok(Acme::Employee->can("boss_id"), "has an accessor for the fk property."); ok(Acme::Employee->can("boss"), "has an accessor for the object."); my $b1 = Acme::Boss->create(name => "Bossy", id => 1000); ok($b1, "made a boss"); my $b2 = Acme::Boss->create(name => "Crabby", id => 2000); ok($b2, "made another boss"); ok($b1 != $b2, "boss objects are different"); ok($b1->id != $b2->id, "boss ids are different"); my $e = Acme::Employee->create(name => "Shifty", id => 3000, boss_id => $b1->id); ok($e, "made an employee"); is($e->boss_id,$b1->id, "the boss is assigned correctly when using the id at creation time and getting the id"); is($e->boss,$b1, "the boss is assigned correctly when using the id at creation time and getting the object"); is($e->boss($b2),$b2, "assigned a different boss object"); is($e->boss_id, $b2->id, "boss id is okay"); is($e->boss, $b2, "boss object is okay"); is($e->boss(undef), undef, "Set the boss to undef"); is($e->boss_id, undef, "No boss_id on the new employee"); is($e->boss, undef, "No boss on the new employee"); is($e->boss($b1), $b1, "Set the boss back to a real object"); is($e->boss,$b1, "the boss is object is back"); is($e->boss_id, $b1->id, "boss id is back too"); is($e->boss_id(undef), undef, "Set the id to undef"); is($e->boss_id, undef, "No boss_id on the new employee"); is($e->boss, undef, "No boss on the new employee"); my $e2 = Acme::Employee->create(name => "Nappy"); ok($e2, "Made a new employee"); is($e2->boss_id, undef, "No boss_id on the new employee"); is($e2->boss, undef, "No boss on the new employee"); is($e->boss($b1), $b1, "set one boss to one object"); is($e2->boss($b2), $b2, "set another boss to the other object"); ok($e->boss != $e2->boss, "boss objects differ as expected"); my $e3 = Acme::Employee->create(name => "Snappy", boss => $b1); ok($e3, "Made a new employee with a boss property"); is($e3->boss, $b1, "No boss on the new employee"); is($e3->boss_id, $b1->id, "No boss_id on the new employee"); 17b_mk_rw_accessor_signals_property_change.t100664023532023421 176112544604517 23653 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use above 'UR'; use Test::More; package Car; class Car { has => [ make => { is => 'Text', }, codeword => { is => 'Text', is_classwide => 1 }, ], }; sub make { my $self = shift; if (@_) { my $value = shift; $self->__make($value); } return $self->__make; } package main; my $car = Car->create(make => 'GM'); isa_ok($car, 'Car'); my $observer_ran = 0; $car->add_observer( aspect => 'make', callback => sub { $observer_ran = 1 }, ); is($observer_ran, 0, 'observer has not run yet'); $car->make('Ford'); is($car->make, 'Ford', 'make changed to Ford'); is($observer_ran, 1, 'observer triggered from make change'); my $classwide_observer_ran = 0; $car->add_observer( aspect => 'codeword', callback => sub { $classwide_observer_ran++ }, ); ok($car->codeword('tomato'),'Change classwide property'); is($classwide_observer_ran, 1, 'classwide property observer fired'); done_testing(); 17c_rw_property_alias.t100664023532023421 1233512544604517 17446 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 42; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created person table'); ok( $dbh->do("create table car (car_id integer PRIMARY KEY, make varchar NOT NULL, owner_id integer NOT NULL REFERENCES person(person_id))"), 'Created car table'); ok( $dbh->do("insert into person values(1, 'Henry')"), 'Insert person 1'); ok( $dbh->do("insert into person values(2, 'Louis')"), 'Insert person 2'); ok( $dbh->do("insert into person values(3, 'Walter')"), 'Insert person 3'); ok( $dbh->do("insert into person values(4, 'Frederick')"), 'Insert person 4'); ok( $dbh->do("insert into car values(1, 'Ford', 1)"), 'Insert car 1'); ok( $dbh->do("insert into car values(2, 'GM', 2)"), 'Insert car 2'); ok( $dbh->do("insert into car values(3, 'Chrysler', 3)"), 'Insert car 3'); ok( $dbh->do("insert into car values(4, 'Duesenberg', 4)"), 'Insert car 4'); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Person', id_by => [ 'person_id' ], has => [ 'name' => { is => 'String' }, 'mark' => { via => '__self__', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Car', id_by => [ car_id => { is => 'Integer' }, ], has_mutable => [ make => { is => 'UR::Value::Text' }, manufacturer => { is => 'String', via => '__self__', to => 'make' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, titleholder => { via => '__self__', to => 'owner' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', ); # Try calling the alias methods my $car = URT::Car->get(manufacturer => 'GM'); ok($car, 'Got car 2 filtered by manufacturer'); is($car->id, 2, 'It is the correct car'); $car = URT::Car->get(make => 'Ford'); ok($car, 'Got car 1 via "make"'); my $another_car = URT::Car->get(manufacturer => 'Ford'); ok($another_car, 'Got car 1 via "manufacturer'); is($car, $another_car, 'They are the same car'); ok($car->make('Honda'), 'Change make'); is($car->make, 'Honda', '"make" is updated'); is($car->manufacturer, 'Honda', '"manufacturer" is the same'); ok($car->manufacturer('Toyota'), 'Change manufacturer'); is($car->make, 'Toyota', '"make" is updated'); is($car->manufacturer, 'Toyota', '"manufacturer" is the same'); # Try querying by different kinds of properties $car = URT::Car->get('owner.name' => 'Walter'); ok($car, 'Got a car via owner.name'); is($car->make, 'Chrysler', 'It is the right car'); $car = URT::Car->get('titleholder.mark', 'Frederick'); ok($car, 'Got a car via titleholder.mark'); is($car->make, 'Duesenberg', 'It is the right car'); # Try creating something new my $bmw_car = URT::Car->create(id => 10, make => 'BMW', owner_id => 1); ok($bmw_car, 'Created new car with "make"'); is($bmw_car->make, 'BMW', '"make" returns correct value'); is($bmw_car->manufacturer, 'BMW', '"manufacturer" returns correct value'); my $audi_car = URT::Car->create(id => 11, manufacturer => 'Audi', owner_id => 1); ok($audi_car, 'Created new car with "manufacturer"'); is($audi_car->make, 'Audi', '"make" returns correct value'); is($audi_car->manufacturer, 'Audi', '"manufacturer" returns correct value'); ok(UR::Context->commit(), 'Commit changes'); my $sth = $dbh->prepare('select * from car'); $sth->execute(); my $results = $sth->fetchall_hashref('car_id'); is_deeply($results, { 1 => { car_id => 1, make => 'Toyota', owner_id => 1 }, 2 => { car_id => 2, make => 'GM', owner_id => 2 }, 3 => { car_id => 3, make => 'Chrysler', owner_id => 3 }, 4 => { car_id => 4, make => 'Duesenberg', owner_id => 4 }, 10 => { car_id => 10, make => 'BMW', owner_id => 1 }, 11 => { car_id => 11, make => 'Audi', owner_id => 1 } }, 'Data was saved to the DB properly'); # Try with some non-standard property definitions UR::Object::Type->define( class_name => 'URT::Owner', id_by => 'owner_id', has => ['name'], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String'}, owner => { is => 'URT::Owner' }, # no id_by titleholder => { via => '__self__', to => 'owner' }, ], ); my $owner = URT::Owner->create(name => 'Bob'); ok($owner, 'Created an Owner'); my $thing = URT::Thing->create(name => 'Thingy'); ok($thing, 'Created a Thing'); ok($thing->owner($owner), 'Assigned an owner to the thing'); # The next get() will generate an error message, suppress it URT::Thing->__meta__->property('owner')->dump_error_messages(0); my $thing2 = URT::Thing->get('owner.name' => 'Bob'); ok($thing2, 'Got a thing via owner.name'); is($thing2->id, $thing->id, 'It is the right Thing'); $thing2 = URT::Thing->get('titleholder.name' => 'Bob'); ok($thing2, 'Got a thing via titleholder.name'); is($thing2->id, $thing->id, 'It is the right Thing'); 18_indirect_accessor.t100664023532023421 475412544604517 17210 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 19; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); UR::Object::Type->define( class_name => "Acme::Boss", has => [ id => { type => "Number" }, name => { type => "String" }, company => { type => "String" }, ] ); UR::Object::Type->define( class_name => 'Acme::Employee', has => [ name => { type => "String" }, boss => { type => "Acme::Boss", id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'name' }, company => { via => 'boss' }, ] ); my $b1 = Acme::Boss->create(name => "Bosser", company => "Some Co."); ok($b1, "created a boss object"); my $e1 = Acme::Employee->create(boss => $b1); ok($e1, "created an employee object"); ok($e1->can("boss_name"), "employees can check their boss' name"); ok($e1->can("company"), "employees can check their boss' company"); is($e1->boss_name,$b1->name, "boss_name check works"); is($e1->company,$b1->company, "company check works"); $b1->name("Crabber"); $b1->company("Other Co."); is($e1->boss_name,$b1->name, "boss_name check works again"); is($e1->company,$b1->company, "company check still works"); my $b2 = Acme::Boss->create(name => "Chief", company => "Yet Another Co."); ok($b2, "made another boss"); $e1->boss($b2); is($e1->boss,$b2, "re-assigned the employee to a new boss"); is($e1->boss_name,$b2->name, "boss_name check works"); is($e1->company,$b2->company, "company check works"); # Hmmm... this only triggered the bug on DataSources backed by a real database my @matches = Acme::Employee->get(boss => 'nonsensical'); ok(scalar(@matches) == 0, 'get employees by boss without boss objects correctly returns 0 items'); my $e2 = Acme::Employee->create(name => 'Bob', boss_name => 'Chief'); ok($e2, 'created an employee via a boss_name that already exists'); is($e2->boss_id, $b2->id, 'boss_id of new employee is correct, did not make a new Acme::Boss'); my %existing_boss_ids = map { $_->id => $_ } Acme::Boss->get(); my $e3 = Acme::Employee->create(name => 'Freddy', boss_name => 'New Boss'); ok($e3, 'Created an employee via a boss_name that did not previously exist'); ok($e3->boss_id, 'it has a boss_id'); ok($e3->boss, 'it has a boss object'); ok(! exists $existing_boss_ids{$e3->boss_id}, 'The new boss_id did not exist before creating this employee'); 19_calculated_accessor.t100664023532023421 1452412544604517 17525 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 41; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); our $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Employee', has => [ first_name => { type => "String" }, last_name => { type => "String" }, full_name => { calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', }, user_name => { calculate_from => ['first_name','last_name'], calculate => 'lc(substr($first_name,0,1) . substr($last_name,0,5))', }, email_address => { calculate_from => ['user_name'] }, cached_uc_full_name => { is_constant => 1, calculate => q( $main::calculate_called = 1; return uc($self->full_name); ), }, ] ); sub Acme::Employee::email_address { my $self = shift; return $self->user_name . '@somewhere.tv'; } $calculate_called = 0; my $e1 = Acme::Employee->create(first_name => "John", last_name => "Doe"); ok($e1, "created an employee object"); ok($e1->can("full_name"), "employees have a full name"); ok($e1->can("user_name"), "employees have a user_name"); ok($e1->can("email_address"), "employees have an email_address"); is($e1->full_name,"John Doe", "name check works"); is($e1->user_name, "jdoe", "user_name check works"); is($e1->email_address, 'jdoe@somewhere.tv', "email_address check works"); is($calculate_called, 0, 'The cached calculation sub has not been called yet'); $calculate_called = 0; my $saved_uc_full_name = uc($e1->full_name); is($e1->cached_uc_full_name, $saved_uc_full_name, 'calculated + cached upper-cased name is correct'); is($calculate_called, 1, 'The calculation function was called'); $e1->first_name("Jane"); $e1->last_name("Smitharoonie"); is($e1->full_name,"Jane Smitharoonie", "name check works after changes"); is($e1->user_name, "jsmith", "user_name check works after changes"); is($e1->email_address, 'jsmith@somewhere.tv', "email_address check works"); $calculate_called = 0; is($e1->cached_uc_full_name, $saved_uc_full_name, 'calculated + cached upper-cased name is correct'); is($calculate_called, 0, 'The calculation function was not called'); isnt($e1->cached_uc_full_name, uc($e1->full_name), 'it is correctly different than the current upper-case full name'); UR::Object::Type->define( class_name => "Acme::LineItem", has => [ quantity => { type => 'Number' }, unit_price => { type => 'Money' }, sum_total => { type => 'Money', calculate => 'sum', calculate_from => ['quantity','unit_price'] }, sub_total => { type => 'Money', calculate => 'product', calculate_from => ['quantity','unit_price'] }, ], ); my $line = Acme::LineItem->create(quantity => 5, unit_price => 2); ok($line, "made an order line item"); is($line->sum_total,7, "got the correct sum-total"); is($line->sub_total,10, "got the correct sub-total"); # Make a cached+calculated property that is also saved in the database use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table thing (thing_id integer, name varchar, munged_name varchar)'); $dbh->do("insert into thing values (1234,'Bob', 'munged Bob')"); $dbh->do("Insert into thing values (2345,'Fred', null)"); $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::SavedThing', id_by => 'thing_id', has => [ name => { is => 'String' }, munged_name => { is_mutable => 0, column_name => 'munged_name', calculate_from => ['name'], calculate => sub { my($name) = @_; $calculate_called = 1; return uc($name) }, }, name2 => { calculate_from => ['__self__'], calculate => sub { return $_[0]->name }, }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); $calculate_called = 0; my $new_thing = Acme::SavedThing->create(name => 'Foo'); ok($new_thing, 'Created a SavedThing'); ok($calculate_called, 'Its calculation sub was called'); $calculate_called = 0; is($new_thing->munged_name, 'FOO', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was not called again'); ok(! eval { $new_thing->munged_name('Something else') }, 'Changing munged_name correctly returned false'); ok($@, 'Trying to change munged_name generated an exception'); $calculate_called = 0; $new_thing = Acme::SavedThing->create(name => 'Bar', munged_name => 'Something else'); ok($new_thing, 'Created another SavedThing'); is($calculate_called, 0, 'The calculation sub was not called'); is($new_thing->munged_name, 'Something else', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was still not called'); $calculate_called = 0; $new_thing = Acme::SavedThing->get(name => 'Bob'); ok($new_thing, 'Got a SavedThing from the DB'); is($new_thing->munged_name, 'munged Bob', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was not called'); $calculate_called = 0; $new_thing = Acme::SavedThing->get(name => 'Fred'); ok($new_thing, 'Got another SavedThing from the DB'); is($new_thing->munged_name, undef, 'The munged_name property is correctly undef'); is($calculate_called, 0, 'The calculation sub was not called'); is($new_thing->name, $new_thing->name2, 'calling calculated sub where calculate_from includes __self__ works'); ok(UR::Context->commit, 'Saved to the DB'); my @row = $dbh->selectrow_array(q(select thing_id, name, munged_name from thing where name = 'Foo')); ok(scalar(@row), 'Retrieved row from DB where name is Foo'); is($row[2], 'FOO', 'Saved munged_name is correct'); @row = $dbh->selectrow_array(q(select thing_id, name, munged_name from thing where name = 'Bar')); ok(scalar(@row), 'Retrieved row from DB where name is Bar'); is($row[2], 'Something else', 'Saved munged_name is correct'); 20_has_many.t100664023532023421 1000412544604517 15316 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 22; UR::Object::Type->define( class_name => 'Acme', is => 'UR::Namespace' ); UR::Object::Type->define( class_name => 'Acme::Order', table_name => 'order_', id_by => [ order_id => { is => 'integer', is_optional => 1, column_name => 'order_id' }, ], has_many => [ lines => { is => 'Acme::OrderLine' }, line_quantities => { via => 'lines', to => 'quantity' }, ], ); UR::Object::Type->define( class_name => 'Acme::OrderBuddy', id_by => [ order => { is => 'Acme::Order', id_by => 'order_id', constraint_name => 'order_line' }, line_num => { is => 'Integer', is_optional => 1, column_name => 'line_num' }, ], ); UR::Object::Type->define( class_name => 'Acme::OrderLine', table_name => 'order_line', id_by => [ order => { is => 'Acme::Order', id_by => 'order_id', constraint_name => 'order_line' }, line_num => { is => 'Integer', is_optional => 1, column_name => 'line_num' }, ], has => [ quantity => { is => 'Integer', is_optional => 1, column_name => 'quantity' }, product => { is => 'String', constraint_name => 'order_line_product' }, ], ); my $o = Acme::Order->create(id => 1); ok($o, "order object created"); my $line1 = Acme::OrderLine->create(order => $o, line_num => 1, quantity => 100, product => "fish"); my $line2 = Acme::OrderLine->create(order => $o, line_num => 2, quantity => 200, product => "fish"); my $line3 = Acme::OrderLine->create(order => $o, line_num => 3, quantity => 300, product => "fish"); my @lines = sort Acme::OrderLine->get(order => $o); is(scalar(@lines), 3, "created expected list of 3 line items"); ok($o->can("line"), "can do line"); ok($o->can("lines"), "can do lines"); ok($o->can("line_list"), "can do line_list"); ok($o->can("line_arrayref"), "can do line_arrayref"); ok($o->can("add_line"), "can do add_line"); ok($o->can("remove_line"), "can do remove_line"); my @r1 = sort $o->lines(); is_deeply(\@r1,\@lines,"lines() works"); my @q1 = sort $o->line_quantities; my @q1_expected = sort map { $_->quantity } @lines; is_deeply(\@q1,\@q1_expected,"indirect method (line_quantities()) returns lists through the lines() acccessor"); my @r2 = sort $o->line_list(); is_deeply(\@r2,\@lines,"line_list() works"); my @r3 = sort @{ $o->line_arrayref() }; is_deeply(\@r3,\@lines,"line_arrayref() works"); my @r4; eval { @r4 = $o->line(line_num => 1) }; is($r4[0], $line1, "line() works with a simple rule"); my $r5; eval { $r5 = $o->line(2) }; is($r5, $line2, "line() returns a single selected item"); my $line4 = $o->add_line(line_num => 4, quantity => 400, product => "fish"); ok($line4, "added a line with full additional parameters"); my @r6 = sort { $a->line_num <=> $b->line_num } $o->lines(); is(scalar(@r6),4,"line count is correct"); my $line5 = $o->add_line(5); ok($line5, "added a line with a partial identity"); my @r7 = sort { $a->line_num <=> $b->line_num } $o->lines(); is(scalar(@r7),5,"line count is correct"); $line5->product('fish'); # Sets the property's value, since it's not is_optional my $removed = $o->remove_line(3); ok($removed, "removed a line with a partial identity"); my @r8 = sort map { $_->line_num } $o->lines(); is("@r8","1 2 4 5","line numbers left are correct"); my $removed2 = $o->remove_line(quantity => 400); ok($removed2, "removed a line with full parameters"); my @r9 = sort map { $_->line_num } $o->lines(); is("@r9","1 2 5","line numbers left are correct"); =cut # This only works if there is a data source currently, # since the whole closure logic is inside of UR::DataSource::RDBMS. ok($o->can("line_iterator"), "can do line_iterator"); my $i = $o->line_iterator; ok($i, "got an iterator"); my @o4; if ($i) { while (my $next = $i->next) { push @o4, $next; } } is_deeply(\@o4,\@lines,"line_iterator works"); =cut 1; 20a_has_many_with_multiple_ids.t100664023532023421 524012544604517 21252 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT; use Test::More tests => 11; UR::Object::Type->define( class_name => 'URT::Order', table_name => 'orders', id_by => [ order_id => { is => 'integer', is_optional => 1, column_name => 'order_id' }, ], has_many => [ attributes => { is => 'URT::OrderAttribute', reverse_as => 'order' }, tracking_number => { is => 'String', via => 'attributes', to => 'value', where => [key => 'tracking_number'], is_mutable => 1}, ship_date => { is => 'String', via => 'attributes', to => 'value', where => [key => 'ship_date'], is_mutable => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::OrderAttribute', id_by => [ order => { is => 'URT::Order', id_by => 'order_id' }, key => { is => 'String' }, value => { is => 'String' }, ], table_name => 'order_attributes', data_source => 'URT::DataSource::SomeSQLite', ); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do("create table orders (order_id integer NOT NULL PRIMARY KEY)"); $dbh->do("create table order_attributes ( order_id integer NOT NULL references orders(order_id), key varchar NOT NULL, value varchar NOT NULL, PRIMARY KEY(order_id, key,value))"); $dbh->do("insert into orders values (99)"); $dbh->do("insert into order_attributes values (99,'tracking_number','abc123')"); $dbh->do("insert into order_attributes values (99,'ship_date','2011 Jan 1')"); my $o = URT::Order->get(99); ok($o, 'Retrieved an order'); is($o->tracking_number, 'abc123', 'tracking_number attribute is OK'); is($o->ship_date, '2011 Jan 1', 'ship_date attribute is OK'); $o = URT::Order->create(id => 1); ok($o, "order object created"); ok($o->add_attribute(key => 'tracking_number', value => 'xyzzy'), 'Added tracking number attribute'); ok($o->add_ship_date('2011 Jan 7'), 'Added ship date'); ok(UR::Context->commit(), 'Commit'); my $rows = $dbh->selectrow_arrayref('select * from orders where order_id = 1'); ok($rows, 'Got row for order 1 from DB'); is($rows->[0], 1,'order_id is correct'); $rows = $dbh->selectall_arrayref('select * from order_attributes where order_id = 1 order by key'); ok($rows, 'Got attributes for order_id 1'); my $expected = [ [1,'ship_date','2011 Jan 7'], [1,'tracking_number','xyzzy']]; is_deeply($rows, $expected, 'Attribute data is ok'); 21_observer.t100664023532023421 2043312544604517 15356 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 42; UR::Object::Type->define( class_name => 'URT::Parent', is_abstract => 1, valid_signals => [qw(last_name something_else)], ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::Parent', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], ); my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $change_count = get_change_count(); my $observations = {}; $p1->last_name("DoDo"); is_deeply($observations, {}, "no callback count change with no observers defined"); is(get_change_count(), $change_count + 1, '1 change recorded even with no observers'); foreach my $thing ( $p1,$p2,'URT::Person','URT::Parent') { foreach my $aspect ( '','last_name','something_else' ) { my $id = ref($thing) ? $thing->id : $thing; my %args = ( callback => sub { no warnings 'uninitialized'; $observations->{$id}->{$aspect}++ } ); if ($aspect) { $args{'aspect'} = $aspect; } ok($thing->add_observer(%args), "Made an observer on $thing for aspect $aspect"); } } $change_count = get_change_count(); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply($observations, { 1 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Do"),"Do", "changed person 2"); is_deeply($observations, { 2 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; ok($p2->__signal_observers__('something_else'),'send the "something_else" signal to person 2'); is_deeply($observations, { 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_observers__('something_else'), 'Send the "something_else" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1, 'something_else' => 1}, 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; # Signals don't propagate down the inheritance tree, only up ok(URT::Parent->__signal_observers__('something_else'), 'Send the "something_else" signal to the URT::Parent class'); is_deeply($observations, { 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_observers__('blablah'), 'Send the "blahblah" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1,}, 2 => { '' => 1,}, 'URT::Person' => { '' => 1,}, 'URT::Parent' => { '' => 1,}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); ok(scalar($p1->remove_observers()), 'Remove observers for Person 1'); $change_count = get_change_count(); $observations = {}; is($p1->last_name("Doooo"),"Doooo", "changed person 1"); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 } }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Boo"),"Boo", "changed person 2"); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, 2 => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); subtest 'once observers' => sub { plan tests => 12; my($parent_observer_fired, $person_observer_fired) = (0,0); ok(my $person_obs = URT::Person->add_observer(aspect => 'last_name', once => 1, callback => sub { $person_observer_fired++ } ), 'Add once observer to "last_name" aspect of URT::Person'); ok(my $parent_obs = URT::Parent->add_observer(aspect => 'last_name', once => 1, callback => sub { $parent_observer_fired++ } ), 'Add once observer to "last_name" aspect of URT::Parent'); $observations = {}; ok($p1->last_name('once'), 'changed person 1'); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Regular callbacks were fired') or diag explain $observations; is($parent_observer_fired, 1, '"once" observer on URT::Parent was fired'); is($person_observer_fired, 1, '"once" observer on URT::Person was fired'); isa_ok($person_obs, 'UR::DeletedRef', 'Person observer is deleted'); isa_ok($parent_obs, 'UR::DeletedRef', 'Parent observer is deleted'); ($parent_observer_fired, $person_observer_fired) = (0,0); $observations = {}; ok($p1->last_name('once again'), 'changed person 1'); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Regular callbacks were fired') or diag explain $observations; is($parent_observer_fired, 0, '"once" observer on URT::Parent was not fired'); is($person_observer_fired, 0, '"once" observer on URT::Person was not fired'); }; subtest 'once observer is removed before callback run' => sub { plan tests => 5; my $obj = URT::Person->create(first_name => 'bob', last_name => 'schmoe'); my $callback_run = 0; our $in_observer = 0; my $observer = $obj->add_observer(aspect => 'first_name', once => 1, callback => sub { my($obj, $aspect, $old, $new) = @_; local $in_observer = $in_observer + 1; die "recursive call to observer" if $in_observer > 1; $obj->$aspect($new . $new); # double up the new value $callback_run++; } ); $obj->first_name('changed'); is($obj->first_name, 'changedchanged', 'Observer modified the new value'); is($callback_run, 1, 'callback was run once'); isa_ok($observer, 'UR::DeletedRef', 'Observer is deleted'); $callback_run = 0; $obj->first_name('bob'); is($obj->first_name, 'bob', 'Changed value back'); is($callback_run, 0, 'Callback was not run'); }; sub get_change_count { my @c = map { scalar($_->__changes__) } URT::Person->get; my $sum = 0; do {$sum += $_ } foreach (@c); return $sum; } 21b_load_observer_autosubclass.t100664023532023421 1075112544604517 21311 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 27; # Make an abstract class with a table, and a child class with a table # The 'load' signal should only ever be fired once for each object loaded my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, subclass_name varchar)'), 'create person table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 'URT::Employee')"), 'insert into person table'); ok($dbh->do('CREATE TABLE employee (employee_id integer, office varchar)'), 'create employee table'); ok($dbh->do("INSERT into employee VALUES (1, '1')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (2, '2')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (3, '3')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (4, '4')"), 'insert into employee table'); UR::Object::Type->define( class_name => 'URT::Person', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'person_id', has => [ name => { is => 'String' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Employee', is => 'URT::Person', id_by => 'employee_id', has => [ office => { is => 'String'} , ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'employee', ); my @person_observations; my $person_obv = URT::Person->add_observer(callback => sub { my($obj,$method) = @_; push @person_observations, [$method, $obj->class, $obj->id]; }); ok($person_obv, "made an observer on Person class"); my @employee_observations; my $employee_obv = URT::Employee->add_observer(callback => sub { my($obj,$method) = @_; push @employee_observations, [$method, $obj->class, $obj->id]; }); ok($employee_obv, "made an observer on Employee class"); @person_observations = (); @employee_observations = (); my $person = URT::Person->get(1); ok($person, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 1] ], # subclasses/loaded as Employee 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 1] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); $person = URT::Employee->get(2); ok($person, 'Got Employee ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 2] ], 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ [ 'load', 'URT::Employee', 2] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); my @people = URT::Person->get(); is(scalar(@people), 4, 'Got 4 Person objects'); is(scalar(@person_observations), 2, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Person observations match expected'); is(scalar(@employee_observations), 2, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Employee observations match expected'); 21c_load_observer_abstract_parent.t100664023532023421 1021312544604517 21747 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 22; # Make an abstract class with a table, and a child class with no table of its own. # The 'load' signal should only ever be fired once for each object loaded my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, subclass_name varchar)'), 'create table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 'URT::Employee')"), 'insert into person table'); UR::Object::Type->define( class_name => 'URT::Person', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'person_id', has => [ name => { is => 'String' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Employee', is => 'URT::Person', ); my @person_observations; my $person_obv = URT::Person->add_observer(callback => sub { my($obj,$method) = @_; push @person_observations, [$method, $obj->class, $obj->id]; #print "*** Got $method signal for obj ".$obj->id." named ".$obj->name." in class ".$obj->class."\n"; #print Carp::longmess(); }); ok($person_obv, "made an observer on Person class"); my @employee_observations; my $employee_obv = URT::Employee->add_observer(callback => sub { my($obj,$method) = @_; push @employee_observations, [$method, $obj->class, $obj->id]; }); ok($employee_obv, "made an observer on Employee class"); @person_observations = (); @employee_observations = (); my $person = URT::Person->get(1); ok($person, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 1] ], # subclasses/loaded as Employee 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 1] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); $person = URT::Employee->get(2); ok($person, 'Got Employee ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 2] ], 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ [ 'load', 'URT::Employee', 2] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); my @people = URT::Person->get(); is(scalar(@people), 4, 'Got 4 Person objects'); is(scalar(@person_observations), 2, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Person observations match expected'); is(scalar(@employee_observations), 2, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Employee observations match expected'); 21d_db_entity_observers.t100664023532023421 3322612544604517 17752 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 115; # Test that basic signals get fired off correctly for DB entities my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, rank integer)'), 'create person table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 1)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 2)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 3)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 4)"), 'insert into person table'); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, rank => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); my @person_observations = (); my @person_ghost_observations = (); my @object1_observations = (); my @object2_observations = (); my @ghost1_observations = (); sub clear_observations { @person_observations = (); @person_ghost_observations = (); @object1_observations = (); @object2_observations = (); @ghost1_observations = (); } my $person_obv = URT::Person->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @person_observations, [$obj, $method, @other_args]; }); ok($person_obv, "made an observer on Person class"); # Observations on person1 won't fire after it's deleted because it's a ghost. Make a new # observer for the ghsot class my $person_ghost_obv = URT::Person::Ghost->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @person_ghost_observations, [$obj, $method, @other_args]; }); ok($person_ghost_obv, 'Make observer for URT::Person::Ghost class'); clear_observations(); my $person1 = URT::Person->get(1); ok($person1, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ [$person1, 'load'] ], # subclasses/loaded as Employee 'Person observations match expected'); @object1_observations = (); my $person1_obj_observer = $person1->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @object1_observations, [$obj,$method,@other_args]}); ok($person1_obj_observer, 'made an observer on person id 1'); clear_observations(); my $person2 = URT::Person->get(2); ok($person2, 'Got person ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'person object 1 observer saw no observations'); my $person2_obj_observer = $person2->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @object2_observations, [$obj,$method,@other_args]}); ok($person2_obj_observer, 'made an observer on person id 2'); # Call the rank mutator, but feed it the original value clear_observations(); my $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations from transaction creation'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(1), 'User rank mutator to set the same value'); is(scalar(@person_observations), 0, 'No Person observations from setting the same value'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from setting the same value'); is(scalar(@object2_observations), 0, 'No object 2 observations from setting the same value'); clear_observations(); ok($trans->rollback(), 'Rollback software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction rollback'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction rollback'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction rollback'); # Now set the rank to a new value clear_observations(); $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(2), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 1, 2] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 1, 2] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($trans = UR::Context::Transaction->rollback(), 'rollback'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 2, 1] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 2, 1] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Set the rank to a new value and commit the software transaction clear_observations(); $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(2), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 1, 2] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 1, 2] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($trans = UR::Context::Transaction->commit(), 'Commit software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction commit'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from transaction commit'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Now commit to the underlying context, with no-commit on ok(UR::DBI->no_commit(1), 'Turn on no-commit flag'); clear_observations(); ok(UR::Context->commit, 'Commit to the DB'); is(scalar(@person_observations), 0, 'No Person observations from Context commit with no_commit on'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from Context commit with no_commit on'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Make another change, turn no-commit off, and try committing again clear_observations(); ok($person1->rank(3), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 2, 3] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 2, 3] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); ok(! UR::DBI->no_commit(0), 'Turn off no-commit flag'); clear_observations(); ok(UR::Context->commit, 'Commit to the DB'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'commit'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'commit'] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction commit'); # Delete person object 1, change person 2 and commit clear_observations(); ok($person1->delete, 'Delete person object 1'); my $person1_ghost = URT::Person::Ghost->get(1); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'delete'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 1, 'One Person ghost observations'); is_deeply(\@person_ghost_observations, [ [$person1_ghost, 'create'] ], 'Person ghost observations match expected'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'delete'] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from delete'); my $object1_ghost_obv = $person1_ghost->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @ghost1_observations, [$obj, $method, @other_args]; }); ok($object1_ghost_obv, 'Create observer for now-deleted Person object 1'); clear_observations(); ok($person2->rank(5), 'Change rank of person 2'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person2, 'rank', 2, 5] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations'); is(scalar(@ghost1_observations), 0, 'No ghost 1 observations'); is(scalar(@object2_observations), 1, 'One observation on person object 2'); is_deeply(\@object2_observations, [ [$person2, 'rank', 2, 5] ], 'person 2 object observations match expected'); clear_observations(); ok(UR::Context->commit, 'Commit to DB'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person2, 'commit'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 1, 'One observation on Person Ghost class'); is_deeply(\@person_ghost_observations, [ [$person1_ghost, 'commit'] ], 'Person Ghost observations match expected'); is(scalar(@object1_observations), 0, 'No observations on person 1 object'); is(scalar(@ghost1_observations), 1, 'One observation on person 1 ghost object'); is_deeply(\@ghost1_observations, [ [$person1_ghost, 'commit'] ], 'person ighost object observations match expected'); is(scalar(@object2_observations), 1, 'One observation on person 2 object'); is_deeply(\@object2_observations, [ [$person2, 'commit'] ], 'person 2 object observations match expected'); 1; 21e_old_subscription_api.t100664023532023421 774712544604517 20104 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 28; package URT::Person; UR::Object::Type->define( class_name => 'URT::Person', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], ); sub validate_subscription { my($class,$method) = @_; return 1 if $method eq 'something_else'; return $class->SUPER::validate_subscription($method); } package main; my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $change_count = get_change_count(); my $observations = {}; $p1->last_name("DoDo"); is_deeply($observations, {}, "no callback count change with no observers defined"); is(get_change_count(), $change_count + 1, '1 change recorded even with no observers'); foreach my $thing ( $p1,$p2,'URT::Person') { foreach my $aspect ( '','last_name','something_else' ) { my $id = ref($thing) ? $thing->id : $thing; my %args = ( callback => sub { no warnings 'uninitialized'; $observations->{$id}->{$aspect}++ } ); if ($aspect) { $args{'method'} = $aspect; } #ok($thing->add_observer(%args), "Made an observer on $thing for aspect $aspect"); ok($thing->create_subscription(%args), "Made an observer on $thing for aspect $aspect"); } } $change_count = get_change_count(); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply($observations, { 1 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Do"),"Do", "changed person 2"); is_deeply($observations, { 2 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; ok($p2->__signal_change__('something_else'),'send the "something_else" signal to person 2'); is_deeply($observations, { 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, 'one change recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_change__('something_else'), 'Send the "something_else" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1, 'something_else' => 1}, 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_change__('blablah'), 'Send the "blahblah" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1,}, 2 => { '' => 1,}, 'URT::Person' => { '' => 1,}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); sub get_change_count { my @c = map { scalar($_->__changes__) } URT::Person->get; my $sum = 0; do {$sum += $_ } foreach (@c); return $sum; } 21f_observer_priority.t100664023532023421 670312544604517 17451 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; UR::Object::Type->define( class_name => 'URT::Person', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], valid_signals => ['something_else'], ); my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $observer_counter = 0; $p1->last_name("DoDo"); is($observer_counter, 0, 'No change in the observer counter when no observers are active'); my %observer_records; my $o1_p1 = $p1->add_observer(callback => sub { $observer_records{'o1_p1'} = $observer_counter++ }, aspect => 'last_name', priority => 9); my $o2_p1 = $p1->add_observer(callback => sub { $observer_records{'o2_p1'} = $observer_counter++ }, aspect => 'last_name', priority => 0); my $o3_p2 = $p2->add_observer(callback => sub { $observer_records{'o3_p2'} = $observer_counter++ }, aspect => 'last_name', priority => 8); my $o4_p2 = $p2->add_observer(callback => sub { $observer_records{'o4_p2'} = $observer_counter++ }, aspect => 'last_name', priority => 1); my $o5_c1 = URT::Person->add_observer(callback => sub { $observer_records{'o5_c1'} = $observer_counter++ }, aspect => 'last_name', priority => 7); my $o6_c1 = URT::Person->add_observer(callback => sub { $observer_records{'o6_c1'} = $observer_counter++ }, priority => 2); my $o7_p1 = $p1->add_observer(callback => sub { $observer_records{'o7_p1'} = $observer_counter++ }, priority => 6); my $o8_p1 = $p1->add_observer(callback => sub { $observer_records{'o8_p1'} = $observer_counter++ }, priority => 3); my $o9_p2 = $p2->add_observer(callback => sub { $observer_records{'o9_p2'} = $observer_counter++ }, priority => 5); my $o10_p2 = $p2->add_observer(callback => sub { $observer_records{'o10_p2'} = $observer_counter++ }, priority => 4); $observer_counter = 0; %observer_records = (); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply(\%observer_records, { 'o2_p1' => 0, 'o6_c1' => 1, 'o8_p1' => 2, 'o7_p1' => 3, 'o5_c1' => 4, 'o1_p1' => 5 }, 'Observers fired in the correct order'); ok($o1_p1->priority(-1), 'Change observer priority from lowest to highest'); $observer_counter = 0; %observer_records = (); is($p1->last_name("foo!"),"foo!", "changed person 1"); is_deeply(\%observer_records, { 'o1_p1' => 0, 'o2_p1' => 1, 'o6_c1' => 2, 'o8_p1' => 3, 'o7_p1' => 4, 'o5_c1' => 5 }, 'Observers fired in the correct order'); 21g_subclass_loaded_observer.t100664023532023421 233312544604517 20713 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; my $animal = UR::Object::Type->define( class_name => 'Animal', ); ok($animal, 'defined Animal'); my %subclass_loaded; my $observer = Animal->add_observer( aspect => 'subclass_loaded', callback => sub { my ($classname, $aspect, $subclassname) = @_; $subclass_loaded{$subclassname}++; }, ); ok($observer, 'defined subclass_loaded observer on Animal'); my $cat = UR::Object::Type->define( class_name => 'Cat', is => 'Animal', ); is($cat->class, 'Cat::Type', 'defined Cat'); ok($subclass_loaded{Cat}, q(Animal's subclass_loaded observer fired when Cat was defined)); $cat = UR::Object::Type->define( class_name => 'Tiger', is => 'Cat', ); is($cat->class, 'Tiger::Type', 'defined Tiger'); ok($subclass_loaded{Tiger}, q(Animal's subclass_loaded observer fired when Tiger was defined)); my $rock = UR::Object::Type->define( class_name => 'Rock', ); is($rock->class, 'Rock::Type', 'defined Rock'); ok(!$subclass_loaded{Rock}, q(Animal's subclass_loaded observer did not fire when Rock was defined)); 21h_multi_inherit_observer.t100664023532023421 212112544604517 20434 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; my $parent1_fired; setup_parent('Parent1', \$parent1_fired); my $parent2_fired; setup_parent('Parent2', \$parent2_fired); UR::Object::Type->define( class_name => 'Child', is => ['Parent1', 'Parent2'], ); for my $self ('Child', Child->create()) { ($parent1_fired, $parent2_fired) = (0, 0); is($parent1_fired, 0, 'Parent1 has not fired'); $self->__signal_observers__('Parent1'); is($parent1_fired, 1, 'Parent1 has fired'); is($parent2_fired, 0, 'Parent2 has not fired'); $self->__signal_observers__('Parent2'); is($parent2_fired, 1, 'Parent2 has fired'); } sub setup_parent { my $class_name = shift; my $fired_ref = shift; my $class = UR::Object::Type->define( class_name => $class_name, valid_signals => [$class_name], ); $class_name->add_observer( aspect => $class_name, callback => sub { $$fired_ref++ }, ); } 21i_defaults.t100664023532023421 755612544604517 15502 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 3; use Test::Fatal qw(exception); use_ok('UR::Observer'); subtest 'defaults' => sub { plan tests => 4; my @has_defaults = UR::Observer->has_defaults; ok(@has_defaults > 0, 'got has_defaults'); my $o = UR::Observer->create(callback => sub {}); isa_ok($o, 'UR::Observer', '$o'); my @observer_args = map { $o->$_ } @has_defaults; my @register_callback_args; { no warnings qw(redefine); local *UR::Observer::_insert_record_into_all_change_subscriptions = sub { my $class = shift; my %values; @values{qw(subject_class_name aspect subject_id)} = (shift, shift, shift); my $list = shift; @values{qw(callback note priority id once)} = @$list; @register_callback_args = @values{@has_defaults}; }; my $oid = UR::Observer->register_callback(callback => sub {}); ok($oid, 'registered callback'); } is_deeply(\@register_callback_args, \@observer_args, 'register_callback gets the same defaults as creating an observer'); }; subtest 'exceptions' => sub { plan tests => 5; subtest 'bad subject_class_name' => sub { plan tests => 3; my @o = UR::Observer->get(subject_class_name => 'Foo'); is(scalar(@o), 0, 'no observer exists'); my $exception = exception { UR::Observer->create(callback => sub {}, subject_class_name => 'Foo') }; ok($exception, 'got an exception'); @o = UR::Observer->get(subject_class_name => 'Foo'); is(scalar(@o), 0, 'no observer created'); }; subtest 'bad aspect' => sub { plan tests => 3; my @o = UR::Observer->get(aspect => 'foo'); is(scalar(@o), 0, 'no observer exists'); my $exception = exception { UR::Observer->create(callback => sub {}, aspect => 'foo') }; ok($exception, 'got an exception'); @o = UR::Observer->get(aspect => 'foo'); is(scalar(@o), 0, 'no observer created'); }; subtest 'extra parameter' => sub { plan tests => 3; my $id = UR::Object::Type->autogenerate_new_object_id_uuid; my @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer exists'); my $exception = exception { UR::Observer->create(callback => sub {}, id => $id, foobar => 1) }; ok($exception, 'got an exception'); @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer created'); }; subtest 'missing callback' => sub { plan tests => 3; my $id = UR::Object::Type->autogenerate_new_object_id_uuid; my @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer exists'); my $exception = exception { UR::Observer->create(id => $id) }; ok($exception, 'got an exception'); @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer created'); }; subtest 'undef parameters' => sub { my @param_names = grep { $_ ne 'id' } UR::Observer->required_params_for_register; plan tests => scalar(@param_names) + 1; ok(@param_names > 0, 'got some param names'); for my $param_name (@param_names) { my %params = UR::Observer->defaults_for_register_callback; $params{$param_name} = undef; subtest $param_name => sub { plan tests => 3; my $id = UR::Object::Type->autogenerate_new_object_id_uuid; my @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer exists'); my $exception = exception { UR::Observer->create(callback => sub {}, %params) }; ok($exception, 'got an exception'); @o = UR::Observer->get(id => $id); is(scalar(@o), 0, 'no observer created'); }; } }; }; 21j_register_callback.t100664023532023421 167312544604517 17326 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 10; use_ok('UR::Observer'); my %fired = ( a => 0, b => 0, ); my %id = ( a => UR::Observer->register_callback(callback => sub { $fired{a}++ }), b => UR::Observer->register_callback(callback => sub { $fired{b}++ }), ); ok($id{a}, q(registered callback 'a')); ok($id{b}, q(registered callback 'b')); UR::Object->__signal_observers__('create'); is($fired{a}, 1, q(callback 'a' fired No. 1)); is($fired{b}, 1, q(callback 'b' fired No. 1)); UR::Object->__signal_observers__('create'); is($fired{a}, 2, q(callback 'a' fired No. 2)); is($fired{b}, 2, q(callback 'b' fired No. 2)); ok(UR::Observer->unregister_callback(id => $id{a}), q(unregistered callback 'a')); UR::Object->__signal_observers__('create'); is($fired{a}, 2, q(callback 'a' did not fire again after unregistering 'a')); is($fired{b}, 3, q(callback 'b' did fire again after unregistering 'a')); 22_cached_get_with_subclasses.t100664023532023421 447712544604517 21052 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 21; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; # FIXME - make another test that does something similar but the items are in the DB UR::Object::Type->define( class_name => 'Acme::Person', id_by => ['person_id'], has => ['name'], ); UR::Object::Type->define( class_name => 'Acme::Employee', is => 'Acme::Person', has => [ 'title' ], ); UR::Object::Type->define( class_name => 'Acme::Customer', is => 'Acme::Person', has => [ 'address' ], ); { my $p1 = Acme::Employee->create(person_id => 1, name => 'Bob', title => 'worker'); ok($p1, 'Created employee 1'); ok($p1->isa('Acme::Employee'), 'Employee 1 isa Acme::Employee'); ok($p1->isa('Acme::Person'), 'Employee 1 isa Acme::Person'); ok(! $p1->isa('Acme::Customer'), 'Employee 1 is not a Acme::Customer'); } { my $p2 = Acme::Employee->create(person_id => 2, name => 'Fred', title => 'boss'); ok($p2, 'Created employee 2'); ok($p2->isa('Acme::Employee'), 'Employee 2 isa Acme::Employee'); ok($p2->isa('Acme::Person'), 'Employee 2 isa Acme::Person'); ok(! $p2->isa('Acme::Customer'), 'Employee 2 is not a Acme::Customer'); } { my $p3 = Acme::Customer->create(person_id => 3, name => 'Joe', address => '123 Main St'); ok($p3, 'Created customer'); ok(! $p3->isa('Acme::Employee'), 'Customer is not a Acme::Employee'); ok($p3->isa('Acme::Person'), 'Customer isa Acme::Person'); ok($p3->isa('Acme::Customer'), 'Customer isa Acme::Customer'); } { my $p = Acme::Customer->get(person_id => 3); ok($p, 'Got a Person with the subclass by id'); ok($p->isa('Acme::Person'), 'It is a Acme::Person'); ok($p->isa('Acme::Customer'), 'It is a Acme::Customer'); ok(! $p->isa('Acme::Employee'), 'It is not a Acme::Employee'); } { my $p = Acme::Person->get(person_id => 3); ok($p, 'Got a Person with the base class by id'); ok($p->isa('Acme::Person'), 'It is a Acme::Person'); ok($p->isa('Acme::Customer'), 'It is a Acme::Customer'); ok(! $p->isa('Acme::Employee'), 'It is not a Acme::Employee'); } { my $p = Acme::Employee->get(person_id => 3); is($p, undef, 'Getting an employee with the id of a customer correctly returns nothing'); } 23_id_class_by_accessor.t100664023532023421 542712544604517 17654 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use UR; use Test::More tests => 11; # This test is to reproduce a poor error message that was received # when trying to access an indirect object which contained an invalid # class name. In the previous case this simply died to trying to call # __meta__ on an undefined value within the accessor sub of # mk_id_based_object_accessor. class TestClass { has => [ other_class => { is => 'Text' }, other_id => { is => 'Number' }, other => { is => 'UR::Object', id_class_by => 'other_class', id_by => 'other_id'}, ], }; class RelatedThing { id_by => 'id', has => [ name => { is => 'String' } ], }; my $a = TestClass->create(other_class => 'NonExistent', other_id => '1234'); my $other = eval { $a->other }; ok(! $other, 'Calling id_class_by accessor with bad data threw exception'); like($@, qr(Can't resolve value for 'other' on class TestClass id), 'Exception looks ok'); my $related = RelatedThing->create(name => 'bob'); my $b = TestClass->create(other_class => 'RelatedThing', other_id => $related->id); ok($b, 'Created thing'); is($b->other->id, $related->id, "Thing's other accessor returne the previously created object"); # Wheels are attached to things. # Clocks have wheels. class Clock { has_many => [ wheels => { is => 'Wheel', reverse_as => 'attached_to' } ], }; class Wheel { has => [ attached_to => { is => 'UR::Object', id_class_by => 'attached_to_class', id_by => 'attached_to_id' } ], }; my $clock = Clock->create(); my $clock_wheel0 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my $clock_wheel1 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my $clock_wheel2 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my @clock_wheels = $clock->wheels(); is(scalar(@clock_wheels), 3, 'Clock has 3 wheels'); is($clock_wheels[0]->id, $clock_wheel0->id, 'Wheel 0 has correct ID'); is($clock_wheels[1]->id, $clock_wheel1->id, 'Wheel 1 has correct ID'); is($clock_wheels[2]->id, $clock_wheel2->id, 'Wheel 2 has correct ID'); # Vehicles also have wheels. Motorcycles are vehicles. class Vehicle { is_abstract => 1, has_many => [ wheels => { is => 'Wheel', reverse_as => 'attached_to' } ], }; class Motorcycle { is => 'Vehicle' }; my $moto = Motorcycle->create(); my $moto_wheel0 = Wheel->create(attached_to_class => 'Motorcycle', attached_to_id => $moto->id); my $moto_wheel1 = Wheel->create(attached_to_class => 'Motorcycle', attached_to_id => $moto->id); my @moto_wheels = $moto->wheels(); is(scalar(@moto_wheels), 2, 'Motorcycle has 2 wheels'); is($moto_wheels[0]->id, $moto_wheel0->id, 'Wheel 0 has correct ID'); is($moto_wheels[1]->id, $moto_wheel1->id, 'Wheel 1 has correct ID'); 24_query_by_is_calculated.t100664023532023421 301112544604517 20216 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_type varchar)'), 'created product table'); ok($dbh->do("insert into product values (1,'race car', 'cool')"), 'insert row into product for race car'); ok($dbh->do("insert into product values (2,'pencil','notcool')"), 'insert row into product for pencil'); UR::Object::Type->define( class_name => 'URT::Product', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_type => { is => 'Text' }, is_cool => { is => 'Boolean', calculate_from => 'product_type', calculate => q( return ($product_type eq 'cool') ? 1 : 0 ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); my @p = URT::Product->get(is_cool => 1); is(scalar(@p), 1, 'Got one product that is_cool'); is($p[0]->product_name, 'race car', 'name is correct'); @p = URT::Product->get(is_cool => 0); is(scalar(@p), 1, 'Got one product that is not is_cool'); is($p[0]->product_name, 'pencil', 'name is correct'); @p = URT::Product->get(-hints => ['is_cool']); is(scalar(@p), 2, 'Getting products with -hints => is_cool got 2 items'); 24_query_by_is_transient.t100664023532023421 455312544604517 20140 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_class varchar)'), 'created product table'); ok($dbh->do('create table cool_product ( product_id int NOT NULL PRIMARY KEY, coolness integer )'), 'created cool_product table'); ok($dbh->do("insert into product values (1,'race car', 'URT::Product::Cool')"), 'insert row into product for race car'); ok($dbh->do("insert into cool_product values (1,10)"), 'insert row into cool_product for race car'); ok($dbh->do("insert into product values (2,'pencil','URT::Product::NotCool')"), 'insert row into product for pencil'); UR::Object::Type->define( class_name => 'URT::Product', is_abstract => 1, subclassify_by => 'product_class', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_class => { is => 'Text' }, coolness => { is_abstract => 1, is_transient => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); UR::Object::Type->define( class_name => 'URT::Product::Cool', is => 'URT::Product', id_by => 'product_id', has => [ coolness => { is => 'Number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'cool_product', ); UR::Object::Type->define( class_name => 'URT::Product::NotCool', is => 'URT::Product', id_by => 'product_id', has_constant => [ coolness => { is => 'Number', is_classwide => 1, value => 0 }, ], ); my @p = URT::Product->get('coolness >' => 0); is(scalar(@p), 1, 'Got one product with positive coolness'); isa_ok($p[0], 'URT::Product::Cool'); is($p[0]->product_name, 'race car', 'name is correct'); @p = URT::Product->get(coolness => 0); is(scalar(@p), 1, 'Got one product with zero coolness'); isa_ok($p[0], 'URT::Product::NotCool'); is($p[0]->product_name, 'pencil', 'name is correct'); @p = URT::Product->get('product_name true' => 1, -hints => ['coolness']); is(scalar(@p), 2, 'Getting products with -hints => coolness got 2 items'); 24_query_via_method_call.t100664023532023421 244612544604517 20055 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_type varchar)'), 'created product table'); ok($dbh->do("insert into product values (1,'race car', 'cool')"), 'insert row into product for race car'); ok($dbh->do("insert into product values (2,'pencil','notcool')"), 'insert row into product for pencil'); sub URT::Product::me { my $self = shift; return $self; } UR::Object::Type->define( class_name => 'URT::Product', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_type => { is => 'Text' }, #me_name => { via => 'me', to => 'product_name' }, me_name => { via => '__self__', to => 'product_name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); my @p = URT::Product->get(me_name => 'race car'); is(scalar(@p), 1, 'Got one product that is_cool'); is($p[0]->product_name, 'race car', 'name is correct'); 25_recurse_get.t100664023532023421 736312544604517 16031 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 41; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # Make a tree structure of data: # A # B C # D E # # Another node Z that's not connected to the other tree my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table node ( node_id varchar not null primary key, parent_node_id varchar )'), 'created node table'); my $sth = $dbh->prepare('insert into node values (?,?)'); foreach my $data ( ['A', undef], ['B', 'A'], ['C', 'A'], ['D', 'B'], ['E', 'B'], ['Z', undef ] ) { ok($sth->execute(@$data), 'Insert a row'); } UR::Object::Type->define( class_name => 'URT::Node', id_by => 'node_id', has => [ parent_node_id => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'node', ); my @n; foreach ( 0 .. 1 ) { # first time through, no objects are loaded so it'll hit the DB # second time, everything should be in the object cache with results handled # by Indexes # Retrieve the tree rooted at B @n = URT::Node->get(id => 'B', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 3, 'Three nodes rooted at B'); is_deeply([ sort map { $_->id } @n], ['B','D','E'], 'Nodes were correct'); # Retrieve the tree rooted at A @n = URT::Node->get(id => 'A', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 5, 'Five nodes rooted at A'); is_deeply([ sort map { $_->id } @n], ['A','B','C','D','E'], 'Nodes were correct'); # Retrieve the tree rooted at Z @n = URT::Node->get(id => 'Z', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 1, 'One node rooted at Z'); is_deeply([ sort map { $_->id } @n], ['Z'], 'Nodes were correct'); # Retrieve the tree rooted at Q @n = URT::Node->get(id => 'Q', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 0, 'No nodes with id Q'); } for ( 0 .. 1 ) { # first time through, unload everything. # second time, everything should be in the object cache with results handled # by Indexes if (! $_) { ok(URT::Node->unload(), 'Unload all URT::Node objects'); } # Retrieve the path from E to the root @n = URT::Node->get(id => 'E', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 3, 'Three nodes from E to the root'); is_deeply([ sort map { $_->id } @n], ['A','B','E'], 'Nodes were correct'); # Retrieve the path from C to the root @n = URT::Node->get(id => 'C', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 2, 'Three nodes from C to the root'); is_deeply([ sort map { $_->id } @n], ['A','C'], 'Nodes were correct'); # Retrieve the path from A to the root @n = URT::Node->get(id => 'A', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 1, 'One node from A to the root'); is_deeply([ sort map { $_->id } @n], ['A'], 'Nodes were correct'); # Retrieve the path from Z to the root @n = URT::Node->get(id => 'Z', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 1, 'One node from Z to the root'); is_deeply([ sort map { $_->id } @n], ['Z'], 'Nodes were correct'); # Retrieve the path from Q to the root @n = URT::Node->get(id => 'Q', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 0, 'No nodes from Q to the root'); } 26_indirect_mutator_with_where_via_is_many.t100664023532023421 300712544604517 23671 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 5; class Person::Relationship { id_by => [ person_id => { is => 'Number', implied_by => 'person', }, related_id => { is => 'Number', implied_by => 'related' }, name => { is => 'Text', }, ], has => [ person => { is => 'Person', id_by => 'person_id', }, related => { is => 'Person', id_by => 'related_id' }, ], }; class Person { id_by => [ name => { is => 'Text', }, ], has => [ relationships => { is => 'Person::Relationship', reverse_as => 'person', is_many => 1, is_mutable => 1, is_optional => 1, }, best_friend => { is => 'Person', via => 'relationships', to => 'related', where => [ name => 'best friend', ], is_many => 0, is_mutable => 1, is_optional => 1, } ], }; my $george = Person->create( name => 'George Washington', ); ok($george, 'created George Washington'); my $john = Person->create( name => 'John Adams', ); ok($john, 'created John Adams'); my $james = Person->create( name => 'James Madison', best_friend => $george, ); ok($james, 'created James Madison'); is_deeply($james->best_friend, $george, 'James best friend is set to George in create'); $james->best_friend($john); is_deeply($james->best_friend, $john, 'James best friend is set to John'); 27_get_with_limit_offset.t100664023532023421 1105512544604517 20113 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 7; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table thing ( thing_id integer not null primary key, name varchar )'), 'created node table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); foreach my $i ( 1 .. 100 ) { $sth->execute($i,$i); } ok($sth->finish, 'Insert test data into DB'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => [ name => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); subtest 'get from DB' => sub { _main_test(); }; subtest 'get from cache' => sub { my @o = URT::Thing->get(); # To get everything into the cache _main_test(); }; sub _main_test { plan tests => 7; subtest 'get with limit' => sub { plan tests => 2; my @o = URT::Thing->get(-limit => 5); is(scalar(@o), 5, 'Got 5 things with limit'); my $ids = get_ids(@o); is_deeply($ids, [1..5],'Got the right objects back'); }; subtest 'get with limit and filter' => sub { plan tests => 2; my @o = URT::Thing->get('thing_id >' => 10, -limit => 5); is(scalar(@o), 5, 'Got 5 things with filter and limit'); my $ids = get_ids(@o); is_deeply($ids, [11..15], 'Got the right objects back'); }; subtest 'get with limit, offset and filter' => sub { plan tests => 2; my @o = URT::Thing->get('thing_id <' => 50, -limit => 2, -offset => 10); is(scalar(@o), 2, 'Got two objects with -limit 2 and -offset 10'); my $ids = get_ids(@o); is_deeply($ids, [11,12], 'Got the right objects back'); }; subtest 'get with filter and page' => sub { plan tests => 2; my @o = URT::Thing->get('thing_id <' => 70, -page => [6,3]); is(scalar(@o), 3, 'Got 3 things with -page [6,3]'); my $ids = get_ids(@o); is_deeply($ids, [16,17,18], 'Got the right objects back'); }; subtest 'iterator with filter and limit' => sub { plan tests => 3; my $iter = URT::Thing->create_iterator('thing_id >' => 30, -limit => 5); ok($iter, 'Created iterator with -limit'); my @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 5, 'Got 5 things with iterator'); my $ids = get_ids(@o); is_deeply($ids, [31 .. 35], 'Got the right objects back'); }; subtest 'iterator with filter, limit and offset' => sub { plan tests => 3; my $iter = URT::Thing->create_iterator('thing_id >' => 35, -limit => 3, -offset => 15); ok($iter, 'Created iterator with -limit and -offset'); my @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 3, 'Got 3 things with iterator'); my $ids = get_ids(@o); is_deeply($ids, [51,52,53], 'Got the right objects back'); }; subtest 'iterator with filter and page' => sub { plan tests => 3; my $iter = URT::Thing->create_iterator('thing_id >' => 70, -page => [5,2]); ok($iter, 'Create iterator with -page [5,2]'); my @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 2,'Got 2 things with iterator'); my $ids = get_ids(@o); is_deeply($ids, [79,80], 'Got the right objects back'); }; } subtest 'limit larger than result set' => sub { plan tests => 2; # All objects are already cached in memory at this point my $object_id = 5; my @o = URT::Thing->get(thing_id => $object_id, -limit => 10); is(scalar(@o), 1, 'got one object back'); my $ids = get_ids(@o); is_deeply($ids, [ $object_id ], 'Got the right object back'); }; subtest 'offset larger than result set' => sub { plan tests => 2; my $warning_message; local $SIG{__WARN__} = sub { $warning_message = shift }; my $expected_line = __LINE__ + 1; my @o = URT::Thing->get(thing_id => 5, -offset => 10); is(scalar(@o), 0, 'Got back no objects'); my $file = __FILE__; like($warning_message, qr(-offset is larger than the result list at $file line $expected_line), 'Warning message was as expected'); }; sub get_ids { my @list = map { $_->id} @_; return \@list; } 28_dont_index_delegated_props.t100664023532023421 276112544604517 21076 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; UR::Object::Type->define( class_name => 'Person', has => [ name => { is => 'String' }, attribs => { is => 'PersonAttr', is_many => 1, reverse_as => 'person' }, address => { is => 'String', via => 'attribs', to => 'value', where => [key => 'address'] }, ], ); UR::Object::Type->define( class_name => 'PersonAttr', has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String' }, value => { is => 'String' }, ], ); my $bob = Person->create(name => 'Bob'); my $bob_addr = $bob->add_attrib(key => 'address', value => '123 main st'); my $fred = Person->create(name => 'Fred'); my $fred_addr = $fred->add_attrib(key => 'address', value => '456 oak st'); my @people = Person->get(name => 'Fred'); is(scalar(@people), 1, 'Got 1 person named Fred'); is($people[0], $fred, 'it is the right person'); @people = Person->get(address => '123 main st'); is(scalar(@people), 1, 'Got 1 person with address 123 main st'); is($people[0], $bob, 'it is the right person'); ok($fred_addr->value('789 elm st'), 'Change address for Fred'); @people = Person->get(address => '456 oak st'); is(scalar(@people), 0, 'Got 0 people at Fred\' old address'); is($fred->address, '789 elm st', 'Address for Fred is correct through delegated property'); 29_indirect_calculated_accessor.t100664023532023421 1051312544604517 21401 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 14; ok(setup(), 'Create initial schema, data and classes'); my $boss = URT::Boss->get(1); ok($boss, 'Got boss id 1'); is($boss->full_name, 'Bob Smith', "Boss' full name is correct"); is($boss->upper_first_name, 'BOB', "Boss' first name in all caps (presumedly from SQL)"); my $empl = URT::Employee->get(name => 'Joe'); ok($empl, 'Got an employee'); is($empl->boss_name, 'Bob Smith', "Employee's boss' name is correct"); is($empl->boss_upper_first_name, 'BOB', "Employee's boss' first name in all caps"); $empl = URT::Employee->get(name => 'Foo'); ok($empl, 'Got another employee with a different boss not yet loaded'); is($empl->boss_name, 'Fred Jones', "Employee's boss' name is correct"); is($empl->boss_upper_first_name, 'FRED', "Employee's boss' first name in all caps"); my @e = $boss->employees(); is(scalar(@e),2, "big boss has one employee plus himself"); my $boss2 = URT::Boss->get(2); @e = $boss2->employees(); is(scalar(@e),3, "middle manager has three employees"); @e = $boss2->secret_employees(); is(scalar(@e),2, "middle manager has two secret employees"); ok(cleanup(), 'Removed schema'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle || return; $dbh->do('create table if not exists BOSS (boss_id int, first_name varchar, last_name varchar, company varchar)') || return; $dbh->do('create table if not exists EMPLOYEE (emp_id int, name varchar, is_secret boolean, boss_id int CONSTRAINT boss_fk references BOSS(BOSS_ID))') || return; my $boss_sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company) values (?,?,?,?)') || return; $boss_sth->execute(1, 'Bob', 'Smith', 'CoolCo') || return; $boss_sth->execute(2, 'Fred', 'Jones', 'Data Inc') || return; $boss_sth->finish(); my $employee_sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)') || return; $employee_sth->execute(1,'Joe', 1, 0) || return; $employee_sth->execute(2,'Mike', 1, 0) || return; $employee_sth->execute(3,'Foo', 2, 1) || return; $employee_sth->execute(4,'Bar', 2, 0) || return; $employee_sth->execute(5,'Baz', 2, 1) || return; $dbh->commit() || return; UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, full_name => { calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', }, upper_first_name => { calculate_from => 'first_name', calculate_sql => 'upper(first_name)' }, company => { type => "String" }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, boss => { type => "URT::Boss", id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'full_name' }, boss_upper_first_name => { via => 'boss', to => 'upper_first_name' }, company => { via => 'boss' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } sub cleanup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle || return; $dbh->do('drop table BOSS') || return; $dbh->do('drop table EMPLOYEE') || return; return 1; } 29b_join_calculated_accessor.t100664023532023421 577512544604517 20677 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; # Tests a get() with a delegated property, where the delegation is resolved via a # calculated property ok(setup(), 'Create initial schema, data and classes'); my $emp = URT::Employee->get(1); ok($emp, 'Got employee 1'); my $boss = $emp->boss; ok($boss, 'Got boss for employee 1'); my @emp = URT::Employee->get(company => 'CoolCo'); is(scalar(@emp), 2, 'Got 2 employees of CoolCo'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do('create table BOSS (boss_id int, first_name varchar, last_name varchar, company varchar)'), 'create table BOSS'); ok($dbh->do('create table EMPLOYEE (emp_id int, name varchar, is_secret, int, boss_id int CONSTRAINT boss_fk references BOSS(BOSS_ID))'), 'create table EMPLOYEE'); my $sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company) values (?,?,?,?)'); $sth->execute(1, 'Bob', 'Smith', 'CoolCo'); $sth->execute(2, 'Robert', 'Jones', 'Data Inc'); $sth->finish(); $sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)'); $sth->execute(1,'Joe', 1, 0); $sth->execute(2,'James', 1, 0); $sth->execute(3,'Jack', 2, 1); $sth->execute(4,'Jim', 2, 0); $sth->execute(5,'Jacob', 2, 1); $sth->finish(); ok($dbh->commit(), 'Commit records to DB'); # Bosses are pretty normal UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, company => { type => "String" }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); # An employee's boss is connected through the calculated property calc_boss_id UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, calc_boss_id => { calculate => q( return $self->boss_id ) }, # silly, but it's still a calculation boss => { type => "URT::Boss", id_by => 'calc_boss_id' }, company => { via => 'boss' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } 29c_join_indirect_accessor.t100664023532023421 756512544604517 20377 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 10; # Tests a get() with an indirect property, where the delegation is resolved via # another delegated property ok(setup(), 'Create initial schema, data and classes'); my $emp = URT::Employee->get(1); ok($emp, 'Got employee 1'); my $boss = $emp->boss; is($boss->first_name, 'Bob', 'Got boss for employee 1'); my $company = $emp->company(); is($company->name, 'CoolCo', 'Got company for employee 1'); # For now, this is pretty inefficient. An Employee's company_id is delegated through boss, # which results in a tree structure for its join requirements. my @emp = URT::Employee->get(company_name => 'CoolCo'); is(scalar(@emp), 2, 'Got 2 employees of CoolCo'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do('create table COMPANY (company_id, name varchar)'), 'create table COMPANY'); ok($dbh->do('create table BOSS (boss_id int, first_name varchar, last_name varchar, company_id int REFERENCES company(company_id))'), 'create table BOSS'); ok($dbh->do('create table EMPLOYEE (emp_id int, name varchar, is_secret, int, boss_id int references BOSS(BOSS_ID))'), 'create table EMPLOYEE'); my $sth = $dbh->prepare('insert into COMPANY (company_id, name) values (?,?)'); $sth->execute(1, 'CoolCo'); $sth->execute(2, 'Data Inc'); $sth->finish; $sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company_id) values (?,?,?,?)'); $sth->execute(1, 'Bob', 'Smith', 1); $sth->execute(2, 'Robert', 'Jones', 2); $sth->finish(); $sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)'); $sth->execute(1,'Joe', 1, 0); $sth->execute(2,'James', 1, 0); $sth->execute(3,'Jack', 2, 1); $sth->execute(4,'Jim', 2, 0); $sth->execute(5,'Jacob', 2, 1); $sth->finish(); ok($dbh->commit(), 'Commit records to DB'); UR::Object::Type->define( class_name => 'URT::Company', id_by => 'company_id', has => ['name'], table_name => 'COMPANY', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, company_id => { type => "Number" }, company => { is => 'URT::Company', id_by => 'company_id' }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); # An employee's boss is connected through the calculated property calc_boss_id UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, boss => { type => "URT::Boss", id_by => 'boss_id' }, company_id => { via => 'boss' }, company => { is => 'URT::Company', id_by => 'company_id' }, company_name => { via => 'company', to => 'name' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } 30_calculated_default.t100664023532023421 1434612544604517 17342 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 3; use Test::Fatal qw(exception); use UR qw(); subtest 'class initialization' => sub { plan tests => 4; subtest 'default_value and calculated_default are incompatible' => sub { plan tests => 2; my %thing = ( class_name => 'URT::Thing', has => [ name => { is => 'String', default_value => 'foo', calculated_default => 1, }, ], ); local *URT::Thing::__default_name__ = sub { 'some name' }; my $exception = exception { UR::Object::Type->define(%thing) }; ok($exception, 'got an exception when trying to use `default_value` and `calculated_default`'); delete $thing{has}->[1]->{default_value}; $exception = exception { UR::Object::Type->define(%thing) }; ok(!$exception, 'did not get an exception when trying to use just `calculated_default`') or diag $exception; }; subtest 'calculated_default validates method name' => sub { plan tests => 2; my %thing = ( class_name => 'URT::Thing', has => [ name => { is => 'String', calculated_default => 'some_method', }, ], ); my $exception = exception { UR::Object::Type->define(%thing) }; ok($exception, 'got an exception when trying to use `calculated_default` without method defined'); local *URT::Thing::some_method = sub { 'some name' }; $exception = exception { UR::Object::Type->define(%thing) }; ok(!$exception, 'did not get an exception when trying to use `calculated_default` with method defined') or diag $exception; }; subtest 'calculated_default => 1 defaults to __default_PROP__' => sub { plan tests => 2; my %thing = ( class_name => 'URT::Thing', has => [ name => { is => 'String', calculated_default => 1, }, ], ); my $exception = exception { UR::Object::Type->define(%thing) }; ok($exception, 'got an exception when trying to use `calculated_default` without method defined'); local *URT::Thing::__default_name__ = sub { 'some name' }; $exception = exception { UR::Object::Type->define(%thing) }; ok(!$exception, 'did not get an exception when trying to use `calculated_default` with method defined') or diag $exception; }; subtest 'calculated_default supports coderef' => sub { plan tests => 2; my %thing = ( class_name => 'URT::Thing', has => [ name => { is => 'String', calculated_default => sub { 'some name' }, }, ], ); my $exception = exception { UR::Object::Type->define(%thing) }; ok(!$exception, 'did not get an exception when trying to use `calculated_default` with method defined') or diag $exception; my $thing = URT::Thing->create(); is($thing->name, 'some name', 'got default name'); }; }; subtest 'dynamic default values' => sub { plan tests => 4; my $orig_foo = 'A'; my $foo = $orig_foo; local *URT::ThingWithDefaultSub::__default_name__ = sub { $foo }; UR::Object::Type->define( class_name => 'URT::ThingWithDefaultSub', has => [ name => { is => 'String', calculated_default => 1 }, ], ); my $thing1 = URT::ThingWithDefaultSub->create(); is($thing1->name, $foo, 'thing1 default name was resolved'); $foo++; isnt($foo, $orig_foo, 'foo was changed'); my $thing2 = URT::ThingWithDefaultSub->create(); is($thing2->name, $foo, 'thing2 default name was resolved'); isnt($thing1->name, $thing2->name, 'things have different names'); }; subtest 'with classwide property' => sub { plan tests => 16; my($name_calc_called, $rank_calc_called, $address_calc_called) = (0, 0, 0); my %thing = ( class_name => 'URT::ThingWithClasswide', has_classwide => [ name => { is => 'String', is_constant => 1, calculated_default => sub { $name_calc_called++; 'some name' }, }, rank => { is => 'String', calculated_default => sub { $rank_calc_called++; 'private' }, }, address => { is => 'String', calculated_default => sub { $address_calc_called++, 'main st' }, }, ], ); my $exception = exception { UR::Object::Type->define(%thing) }; ok(!$exception, 'did not get an exception when trying to use `calculated_default` with method defined') or diag $exception; is($name_calc_called, 0, 'name calculation not called yet'); is($rank_calc_called, 0, 'rank calculation not called yet'); is($address_calc_called, 0, 'address calculation not called yet'); is(URT::ThingWithClasswide->name, 'some name', 'got default name'); is($name_calc_called, 1, 'name calculation was called'); is(URT::ThingWithClasswide->rank, 'private', 'got default rank'); is($rank_calc_called, 1, 'rank calculation was called'); ok(URT::ThingWithClasswide->address('foo ln'), 'Set address'); is(URT::ThingWithClasswide->address, 'foo ln', 'Address property was changes'); is($address_calc_called, 0, 'address calculation was not called'); $exception = exception { URT::ThingWithClasswide->name('foo') }; like($exception, qr/Cannot change read-only class-wide property/, 'Got exception trying to change read-only classwide property'); is($name_calc_called, 1, 'name calculation was not called again'); ok(URT::ThingWithClasswide->rank('general'), 'Changed rank'); is(URT::ThingWithClasswide->rank, 'general', 'rank property changed'); is($rank_calc_called, 1, 'name calculation was not called again'); }; 30_default_values.t100664023532023421 2334512544604517 16537 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 84; UR::Object::Type->define( class_name => 'URT::Parent', has => [ name => { is => 'String', default_value => 'Anonymous' }, ], ); UR::Object::Type->define( class_name => 'URT::Child', is => 'URT::Parent', has => [ color => { is => 'String', default_value => 'clear' }, ], ); UR::Object::Type->define( class_name => 'URT::GrandChild', is => 'URT::Child', has => [ name => { is => 'String', default_value => 'Doe' }, ], ); UR::Object::Type->define( class_name => 'URT::SingleChild', is => ['UR::Singleton', 'URT::Child'], ); UR::Object::Type->define( class_name =>'URT::BoolThing', has => [ boolval => { is => 'Boolean', default_value => 1 }, ], ); UR::Object::Type->define( class_name => 'URT::IntThing', has => [ intval => { is => 'Integer', default_value => 100 }, ], ); # Make a pair of classes we'll use to test setting indirect properties at # creation time. The ObjThing has an int_value, through a bridge, to an IntThing's intval UR::Object::Type->define( class_name => 'URT::BridgeThing', has => [ int_thing => { is => 'URT::IntThing', id_by => 'int_thing_id' }, int_value => { via => 'int_thing', to => 'intval' }, ], ); UR::Object::Type->define( class_name => 'URT::ObjThing', has => [ bridge_thing => { is => 'URT::BridgeThing', id_by => 'bridge_thing_id' }, int_value => { via => 'bridge_thing', to => 'int_value', default_value => 1234 }, ], ); UR::Object::Type->define( class_name => 'URT::CommandThing', is => 'Command', has => [ opt => { is => 'Boolean', default_value => 1 }, ], ); my $p = URT::Parent->create(id => 1); ok($p, 'Created a parent object without name'); is($p->name, 'Anonymous', 'object has default value for name'); is($p->name('Bob'), 'Bob', 'We can set the name'); is($p->name, 'Bob', 'And it returns the correct name after setting it'); $p = URT::Parent->create(id => 100, name => undef); ok($p, 'Created a parent object with the empty string for the name'); is($p->name, undef, 'Name is correctly empty'); is($p->name('Joe'), 'Joe', 'We can set it to something else'); is($p->name, 'Joe', 'And it returns the correct name after setting it'); my $o = URT::BoolThing->create(id => 1); ok($o, 'Created a BoolThing without a value'); is($o->boolval, 1, 'it has the default value for boolval'); is($o->boolval(0), 0, 'we can set the value'); is($o->boolval, 0, 'And it returns the correct value after setting it'); $o = URT::BoolThing->create(id => 2, boolval => 0); ok($o, 'Created a BoolThing with the value 0'); is($o->boolval, 0, 'it has the right value for boolval'); is($o->boolval(1), 1, 'we can set the value'); is($o->boolval, 1, 'And it returns the correct value after setting it'); $o = URT::IntThing->create(id => 1); ok($o, 'Created an IntThing without a value'); is($o->intval, 100, 'it has the default value for intval'); is($o->intval(1), 1, 'we can set the value'); is($o->intval, 1, 'And it returns the correct value after setting it'); $o = URT::IntThing->create(id => 2, intval => 0); ok($o, 'Created an IntThing with the value 0'); is($o->intval, 0, 'it has the right value for boolval'); is($o->intval(1), 1, 'we can set the value'); is($o->intval, 1, 'And it returns the correct value after setting it'); $o = URT::ObjThing->create(id => 1); ok($o, 'Created an ObjThing without an int_value'); is($o->int_value, 1234, 'It has the default value for int_value'); ok($o->bridge_thing_id, 'The ObjThing has a bridge_thing_id'); ok($o->bridge_thing, 'We can get its bridge_thing object'); is($o->bridge_thing->id, $o->bridge_thing_id, 'The IDs match for bridge_thing_id and URT::BridgeThing ID param'); $o = $o->bridge_thing; is($o->int_value, 1234, 'The BridgeThing has the correct value for int_value'); ok($o->int_thing, 'We can get its int_thing object'); is($o->int_thing->id, $o->int_thing_id, "The IDs match for the hangoff object"); is($o->int_thing->intval, 1234, "The int_thing's intval is 1234"); $o = URT::ObjThing->create(id => 2, int_value => 9876); ok($o, 'Created ObjThing with int_value 9876'); is($o->int_value, 9876, 'It has the correct value for int_value'); ok($o->bridge_thing_id, 'The ObjThing has a bridge_thing_id'); ok($o->bridge_thing, 'We can get its bridge_thing object'); is($o->bridge_thing->id, $o->bridge_thing_id, 'The IDs match for bridge_thing_id and URT::BridgeThing ID param'); $o = $o->bridge_thing; is($o->int_value, 9876, 'The BridgeThing has the correct value for int_value'); ok($o->int_thing_id, 'The BridgeThing has an int_thing_id value'); ok($o->int_thing, 'We can get its int_thing object'); is($o->int_thing->id, $o->int_thing_id, "The IDs match for the hangoff object"); is($o->int_thing->intval, 9876, "The int_thing's intval is 9876"); my $int_thing = URT::IntThing->get(intval => 1234); ok($int_thing, 'Got the IntThing with intval 1234, again'); $o = URT::ObjThing->create(id => 3); ok($o, 'Created another ObjThing without an int_value'); is($o->int_value, 1234, "The ObjThing's int_value is the default 1234"); ok($o->bridge_thing, "This ObjThing's bridge_thing property has a value"); is($o->bridge_thing->int_thing_id, $int_thing->id, 'The bridge_thing points to the original IntThing having the value 1234'); $p = URT::Parent->create(id => 2, name => 'Fred'); ok($p, 'Created a parent object with a name'); is($p->name, 'Fred', 'Returns the correct name'); my $c = URT::Child->create(); ok($c, 'Created a child object without name or color'); is($c->name, 'Anonymous', 'child has the default value for name'); is($c->color, 'clear', 'child has the default value for color'); is($c->name('Joe'), 'Joe', 'we can set the value for name'); is($c->name, 'Joe', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); $c = URT::GrandChild->create(); ok($c, 'Created a grandchild object without name or color'); is($c->name, 'Doe', 'child has the default value for name'); is($c->color, 'clear', 'child has the default value for color'); is($c->name('Joe'), 'Joe', 'we can set the value for name'); is($c->name, 'Joe', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); $c = URT::SingleChild->_singleton_object; ok($c, 'Got an object for the child singleton class'); is($c->name, 'Anonymous','name has the default value'); is($c->name('Mike'), 'Mike', 'we can set the name'); is($c->name, 'Mike', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); my $cmd = URT::CommandThing->create(); ok($cmd, 'Got a CommandThing object without specifying --opt'); is($cmd->opt, 1, '--opt value is 1'); $cmd = URT::CommandThing->create(opt => 0); ok($cmd, 'Created CommandThing with --opt 0'); is($cmd->opt, 0, '--opt value is 0'); # test oo defaults my $p1 = URT::Parent->get(1); my $p2 = URT::Parent->get(2); class URT::Thing2a { has => [ o1 => { is => 'URT::Parent', default_value => 2 }, ] }; class URT::Thing2b { has => [ o1 => { is => 'URT::Parent', id_by => 'o1_id', default_value => 2 }, ] }; class URT::Thing2c { has => [ o1 => { is => 'URT::Parent', is_many => 1, default_value => [1,2] }, ] }; note("test default values specified as IDs"); my $t1 = URT::Thing2a->create(); is($t1->o1, $p2, "default value is set (no id_by): $p2"); my $t2 = URT::Thing2b->create(); is($t1->o1, $p2, "default value is set (with id_by) $p2"); my $t3 = URT::Thing2c->create(); my @t3o1 = $t3->o1; is("@t3o1", "$p1 $p2", "default value is set to two items on an is_many property"); note("test default values overridden in construction not doing anything"); my $t4 = URT::Thing2a->create(o1 => $p1); is($t4->o1, $p1, "value is set as specified to $p1 not the default $p2"); my $t5 = URT::Thing2b->create(o1 => $p1); is($t5->o1, $p1, "value is set as specified to $p1 not the default $p2 (id_by)"); $DB::single = 1; my $t6 = URT::Thing2c->create(o1 => [$p2]); my @t6o1 = $t6->o1; is("@t6o1", "$p2", "value is set to as specified $p2 no the default of $p1 and $p2 (is_many)"); note("test default values specified as queries"); class URT::Thing3a { has => [ o1 => { is => 'URT::Parent', default_value => { name => "Fred" } }, ] }; class URT::Thing3b { has => [ o1 => { is => 'URT::Parent', id_by => 'o1_id', default_value => { name => "Fred" } }, ] }; class URT::Thing3c { has => [ o1 => { is => 'URT::Parent', is_many => 1, default_value => { name => ["Fred","Bob"] } }, ] }; my $t7 = URT::Thing3a->create(); is($t7->o1, $p2, "default value is $p2 as specified by query"); my $t2q = URT::Thing3b->create(); is($t7->o1, $p2, "default value is $p2 as specified by query"); my $t9 = URT::Thing3c->create(); my @t9o1 = $t9->o1; is("@t9o1", "$p1 $p2", "default value is set to both $p1 and $p2 as specified by query"); SKIP: { skip "UR::Command::sub_command_dirs() complains if there's no module, even if the class exists", 4; my($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv('--opt'); is($cmd_class, 'URT::CommandThing', 'resolved the correct command class'); is($params->{'opt'}, 1, 'Specifying --opt on the command line sets opt param to 1'); ($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv(); is($params->{'opt'}, 1, 'opt option has the default value with no argv arguments'); ($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv('--noopt'); is($params->{'opt'}, 0, 'Specifying --noopt sets opt params to 0'); } 31_ref_as_value.t100664023532023421 1116012544604517 16160 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::ObjWithHash; use strict; use warnings; plan tests => 27; my $o = URT::ObjWithHash->create(myhash1 => { aaa => 111, bbb => 222 }, mylist => [ ccc => 333, ddd => 444 ]); my @h = ($o->myhash1, $o->mylist); is(ref($h[0]),'HASH', "got a hashref back"); is(ref($h[1]),'ARRAY', "got an arrayref back"); is_deeply($h[0],{ aaa => 111, bbb => 222 },"got correct values back for hashref"); is_deeply($h[1],[ ccc => 333, ddd => 444 ],"got correct values back for arrayref"); # make sure things being associated with objects # are not being copied in the constructor class TestClassB { has => [ value => { is => 'String' }, ], }; class TestClassA { has => [ b_thing => { is => 'TestClassB' } ], }; my $ax = TestClassA->create(); ok($ax, "Created TestClassA without b_thing"); my $bx = TestClassB->create( value => 'abcdfeg' ); ok($bx, "Created TestClassB with value"); ok($ax->b_thing($bx), "Set b_thing to TestClassB object"); is($ax->b_thing, $bx, "b_thing is TestClassB object"); my $ay = TestClassA->create( b_thing => $bx ); ok($ay, "Created TestClassA with bx as b_thing"); is($ax->b_thing,$ay->b_thing, "ax->b_thing is ay->b_thing"); ok($bx->value('oyoyoy'), "Changed bx->value"); is($ax->b_thing->value, $ay->b_thing->value, "ax->b_thing value is ay->b_thing value"); my $by = TestClassB->create( value => 'zzzykk' ); ok($by, "Created TestClassB with value"); ok($ay->b_thing($by), "Changed ay b_thing to by"); isnt($ax->b_thing,$ay->b_thing,"ax b_thing is not ay b_thing"); isnt($ax->b_thing->value,$ay->b_thing->value,"ax->b_thing value is not ay->b_thing value"); class TestClassC { has => [ foo => { is => 'ARRAY' } ] }; my $c; ok($c = TestClassC->create,"Created TestClassC with no properties"); ok($c->foo([qw{foo bar baz}]),"Set foo"); is_deeply($c->foo,[qw{foo bar baz}],'Checking array'); ok($c = TestClassC->create( foo => [qw{foo bar baz}] ),"Created TestClassC with foo arrayref"); is_deeply($c->foo,[qw{foo bar baz}],'Checking array for alpha-sort'); #TODO: { # local $TODO = 'somewhere, somehow PAP workflow does this.... so lets make sure it works'; my $d; ok(eval { $d = TestClassC->create( foo => [ $c, ## first element is a ur object [ ## next is a psuedo hash, or something that looks like one { make => 1, perl => 2, mad => 3, at => 4, us => 5 }, 'this', 'is', 'a', 'pseudo', 'hash' ] ] ) }, "created TestClassC with psuedo-hash like array"); # diag "data was: " . Data::Dumper::Dumper($d); #} # make Bar a real class so it is not mistaken for a primitive package Bar; sub bar {}; package main; # new rule logic seems to allow boolexpr references to be cloned for my $c ('Bar') { class Foo { has => [ a => { is => $c }, b => { is => $c }, c => { is => $c } ] }; my @r = map { bless({ id => $_ },$c); } (100..102); my @f = qw/a b c/; my $oo = Foo->define_boolexpr(a => $r[0], c => $r[2], b => $r[1]); my %pp = $oo->params_list; my @pp = @pp{@f}; my $o = $oo->normalize; my %p = $o->params_list; my @p = @p{@f}; my $str = Data::Dumper::Dumper(\@r,\%pp,\%p,$oo,$o); is("@pp", "@r", "unnormalized rule decomposes correctly") or diag $str; is("@p", "@r", "normalized rule decomposes correctly") or diag $str; } my @p = ( 'myhash1', { 'bbb' => 222, 'aaa' => 111 }, 'mylist', [ 'ccc', 333, 'ddd', 444 ], 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', ); my $b = URT::ObjWithHash->define_boolexpr(@p); my $hu = $b->value_for('myhash1'); my $au = $b->value_for('mylist'); note($hu); note($au); my $n = $b->normalize; my $hn = $n->value_for('myhash1'); my $an = $n->value_for('mylist'); is($an,$au,"the normalized array is the same ref as the unnormalized"); is($hn,$hu,"the normalized array is the same ref as the unnormalized"); my %b = $b->params_list; my %n = $n->params_list; my @b = map { $_ => $b{$_}.'' } sort keys(%b); my @n = map { $_ => $n{$_}.'' } sort keys(%n); is("@n","@b", "normalization keeps references correct"); 32_ur_object_id.t100664023532023421 1527512544604517 16171 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; use Data::UUID; subtest 'simple single-id class' => sub { plan tests => 12; my $tc1 = class TestClass1 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], }; my $o = TestClass1->create(foo => 'aaaa', value => '1234'); ok($o, "Created TestClass1 object with explicit ID"); is($o->foo, 'aaaa', "Object's explicit ID has the correct value"); is($o->foo, $o->id, "Object's implicit ID property is equal to the explicit property's value"); $o = TestClass1->create(value => '2345'); ok($o, "Created another TestClass1 object with an autogenerated ID"); ok($o->foo, "The object has an autogenerated ID"); is($o->foo, $o->id, "The object's implicit ID property is equal to the explicit property's value"); my @id_parts = split(' ',$o->id); is($id_parts[0], Sys::Hostname::hostname(), 'hostname part of ID seen'); is($id_parts[1], $$, 'process ID part of ID seen'); # the 2nd part is the time and not reliably checked is($id_parts[3], $UR::Object::Type::autogenerate_id_iter, 'Iterator number part of ID seen'); TestClass1->dump_error_messages(0); TestClass1->queue_error_messages(1); my $error_messages = TestClass1->error_messages_arrayref(); $o = TestClass1->create(foo => 'aaaa', value => '123456'); ok(!$o, "Correctly couldn't create an object with a duplicated ID"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error message'); like($error_messages->[0], qr/An object of class TestClass1 already exists with id value 'aaaa'/, 'The error message was correct'); }; subtest 'dual-id class' => sub { plan tests => 19; class TestClass2 { id_by => ['foo','bar'], has => [ foo => { is => 'String' }, bar => { is => 'String' }, value => { is => 'String' }, ], }; my $o = TestClass2->create(foo => 'aaaa', bar => 'bbbb', value => '1'); ok($o, "Created a TestClass2 object with both explicit ID properties"); is($o->foo, 'aaaa', "First explicit ID property has the right value"); is($o->bar, 'bbbb', "Second explicit ID property has the right value"); is($o->id, TestClass2->__meta__->resolve_composite_id_from_ordered_values('aaaa','bbbb'), "Implicit ID property has the right value"); TestClass2->dump_error_messages(0); TestClass2->queue_error_messages(1); my $error_messages = TestClass2->error_messages_arrayref(); my $composite_id = TestClass2->__meta__->resolve_composite_id_from_ordered_values('c', 'd'); $o = TestClass2->create(id => $composite_id); ok($o, 'Created a TestClass2 object using the composite ID'); is($o->foo, 'c', 'First explicit ID property has the right value'); is($o->bar, 'd', 'Second explicit ID property has the right value'); is($o->id, $composite_id, 'Implicit ID property has the right value'); $o = TestClass2->create(foo => 'qqqq', value => 'blah'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: bar/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(bar => 'wwww', value => 'blah'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs, again"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: foo/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(value => 'asdf'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs, again"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: foo, bar/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(foo => 'aaaa', bar => 'bbbb', value => '2'); ok(!$o, "Correctly couldn't create another object with duplicated ID properites"); my $expected_error_id = TestClass2->__meta__->resolve_composite_id_from_ordered_values('aaaa','bbbb'); like($error_messages->[0], qr/An object of class TestClass2 already exists with id value '$expected_error_id'/, 'The error message was correct'); }; subtest 'parent and child classes' => sub { plan tests => 18; my $tc3 = class TestClass3 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], id_generator => '-uuid', }; my $tc_3_child = class TestClass3Child { is => 'TestClass3', }; for my $class ( 'TestClass3','TestClass3Child' ) { is($class->__meta__->id_generator, '-uuid', "$class uses uuid for IDs"); my $o = $class->create(foo => 'aaaa', value => '1234'); ok($o, "Created TestClass3 object with explicit ID"); is($o->foo, 'aaaa', "Object's explicit ID has the correct value"); is($o->foo, $o->id, "Object's implicit ID property is equal to the explicit property's value"); my $ug = eval { Data::UUID->new->from_hexstring('0x' . $o->foo) }; ok(((! $ug) or ($ug eq pack('x16'))), 'It was not a properly formatted UUID'); $o = TestClass3->create(value => '2345'); ok($o, "Created another TestClass3 object with an autogenerated ID"); ok($o->foo, "The object has an autogenerated ID"); is($o->foo, $o->id, "The object's implicit ID property is equal to the explicit property's value"); $ug = Data::UUID->new->from_hexstring($o->foo); ok($ug, 'It was a properly formatted UUID'); } }; subtest 'custom id generator' => sub { plan tests => 3; my $class_tc4_generator = 0; my $tc4 = class TestClass4 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], id_generator => sub { ++$class_tc4_generator }, }; my $o = TestClass4->create(value => '12344'); ok($o, 'Created TestClass4 object with an autogenerated ID'); is($class_tc4_generator, 1, 'The generator anonymous sub was called'); is($o->id, $class_tc4_generator, 'The object ID is as expected'); }; 33_multiple_inheritance_for_same_table.t100664023532023421 616312544604517 22744 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 10; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, subclass varchar)'), 'created person table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', is_abstract => 1, id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, subclass => { is => 'Text' }, ], subclassify_by => 'subclass', data_source => 'URT::DataSource::SomeSQLite', ), 'Created abstract class for people'); ok(UR::Object::Type->define( class_name => 'URT::Person::WithFavoriteColor', is => 'URT::Person', is_abstract => 1, has_transient_optional => [ favorite_color => { is => 'Text' }, ], ), 'Created abstract subclass for people who temporarily have favorite colors'); ok(UR::Object::Type->define( class_name => 'URT::Person::WithNickname', is => 'URT::Person', is_abstract => 1, has_transient_optional => [ nickname => { is => 'Text' }, ], ), 'Created abstract subclass for people who temporarily have nicknames'); ok(UR::Object::Type->define( class_name => 'URT::StudyParticipant', is => ['URT::Person::WithNickname', 'URT::Person::WithFavoriteColor'], has_transient_optional => [ participant_id => { is => 'Number' }, ], ), 'Created a class of person who is being asked their favorite color and nickname'); # Insert some data so we can query for it my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [111, 'Alice', 'URT::StudyParticipant'] ) { $insert->execute(@$row); } $insert->finish(); my $class = 'URT::StudyParticipant'; can_ok($class, (qw(favorite_color nickname participant_id))); subtest 'SELECT' => sub { plan tests => 5; my $got_select; URT::DataSource::SomeSQLite->add_observer( aspect => 'query', callback => sub { my($ds, $aspect, $sql) = @_; ($got_select) = ($sql =~ m/SELECT\s+(.+)\s+FROM\s/im); }); my @participants = $class->get(); is(scalar(@participants), 1, 'got participants'); isa_ok($participants[0], $class); is($participants[0]->name, 'Alice', 'got name of participant'); is($participants[0]->id, 111, 'got id of participant'); is($got_select, 'PERSON.name, PERSON.person_id, PERSON.subclass', 'SQL select clause'); }; subtest 'INSERT' => sub { plan tests => 2; my $bob = URT::StudyParticipant->create(name => 'Robert', person_id => '112', nickname => 'Bob'); isa_ok($bob, $class); ok(UR::Context->_sync_databases(), 'INSERTed new row to database'); }; subtest 'UPDATE' => sub { plan tests => 2; my $alice = URT::Person->get(name => 'Alice'); is($alice->id, 111, 'found existing user'); $alice->name('Alicia'); ok(UR::Context->_sync_databases(), 'UPDATEd row in database'); }; 34_autouse_with_circular_ur_classdef.t100664023532023421 210112544604517 22461 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; plan skip_all => "known broken - if a parent class has a property of a type which is a subclass of itself, the subclass must explicitly 'use' its parent instead of relying on autoloading"; #plan tests => 2; # KNOWN SOLUTION: # class definition happens in two phases: the minimal phase, then building the detailed meta objects # when one definition triggers loading of other classes, the minimal phase should complete for everything, then the final should run on everythign # this is much like we do when bootstrapping, and if it were in place special boostrapping logic might not be needed # until then: if you do a bunch of cirucular crap with your classes explicitly have them "use" each other :) # make sure things being associated with objects # are not being copied in the constructor use_ok("URT::34Subclass"); my $st = URT::34Subclass->create(); ok($st,"made subclass"); 35_all_objects_are_loaded_subclass.t100664023532023421 1106412544604517 22053 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 21; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); our $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); our $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(); is(scalar(@o), 2, 'URT::Parent->get returned 2 parent objects'); is($load_count, 2, 'loaded 2 Parent objects'); is($query_count, 2, 'get() triggered 2 queries'); # 1 on the parent table, 1 more for child joined to parent $load_count = 0; $query_count = 0; @o = URT::Child->get(); is(scalar(@o), 1, 'URT::Child->get returned 1 child object'); is($load_count, 0, 'correctly loaded 0 objects - gotten from the cache'); is($query_count, 0, 'get() correctly triggered 0 queries'); $load_count = 0; $query_count = 0; @o = URT::OtherChild->get(); is(scalar(@o), 0, 'URT::OtherChild->get returned 0 other child objects'); is($load_count, 0, 'loaded 0 times - all from the cache'); # Note that the original parent get() would have triggered a query joining other_child table # to parent if there were any other_child objects is($query_count, 0, 'get() correctly triggered 0 query'); unlink(URT::DataSource::SomeSQLite->server); # Remove the file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, the_type_name varchar)'), 'created parent table'); ok($dbh->do('create table CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), child_value varchar )'), 'created child table'); ok($dbh->do('create table OTHER_CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), other_child_value varchar )'), 'created other child table'); #@URT::Parent::ISA = ('UR::ModuleBase'); #@URT::Child::ISA = ('UR::ModuleBase'); #@URT::OtherChild::ISA = ('UR::ModuleBase'); #ok(UR::Object::Type->define( # class_name => 'URT', # is => 'UR::Namespace', # ), # "Created namespace for URT"); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'the_type_name' => { is => 'STRING'}, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', table_name => 'CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ child_value => { is => 'STRING' }, ], ), "Created class for Child" ); ok(UR::Object::Type->define( class_name => 'URT::OtherChild', table_name => 'OTHER_CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ other_child_value => { is => 'STRING' }, ], ), "Created class for Other Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values (1, 'Bob', 'URT::Parent'))), "insert a parent object"); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values ( 2, 'Fred', 'URT::Child'))), "Insert part 1 of a child object"); ok($dbh->do(q(insert into child_table (child_id, child_value) values ( 2, 'stuff'))), "Insert part 2 of a child object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return $obj->the_type_name; } 36_superclass_already_loaded.t100664023532023421 1127712544604517 20740 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); our $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); our $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(the_type_name => 'URT::Child'); is(scalar(@o), 1, 'URT::Parent->get returned 1 object'); is($load_count, 1, 'loaded 1 objects'); ok($o[0]->isa('URT::Child'), 'Loaded object is of the correct type'); is($query_count, 2, 'get() triggered 2 queries'); # 1 on the parent table, 1 more for child joined to parent $load_count = 0; $query_count = 0; @o = URT::Child->get(the_type_name => 'URT::Child', child_value => 'stuff'); is(scalar(@o), 1, 'URT::Child->get returned 1 child object'); is($load_count, 0, 'currectly loaded 0 objects - gotten from the cache'); is($query_count, 0, 'get() correctly triggered 0 queries'); $load_count = 0; $query_count = 0; @o = URT::OtherChild->get(); is(scalar(@o), 0, 'URT::OtherChild->get returned 0 other child objects'); is($load_count, 0, 'loaded 0 times - all from the cache'); # Note that the original parent get() would have triggered a query joining other_child table # to parent if there were any other_child objects is($query_count, 1, 'get() correctly triggered 1 query'); unlink(URT::DataSource::SomeSQLite->server); # Remove the DB file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, the_type_name varchar)'), 'created parent table'); ok($dbh->do('create table CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), child_value varchar )'), 'created child table'); ok($dbh->do('create table OTHER_CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), other_child_value varchar )'), 'created other child table'); #@URT::Parent::ISA = ('UR::ModuleBase'); #@URT::Child::ISA = ('UR::ModuleBase'); #@URT::OtherChild::ISA = ('UR::ModuleBase'); #ok(UR::Object::Type->define( # class_name => 'URT', # is => 'UR::Namespace', # ), # "Created namespace for URT"); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'the_type_name' => { is => 'STRING'}, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', table_name => 'CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ child_value => { is => 'STRING' }, ], ), "Created class for Child" ); ok(UR::Object::Type->define( class_name => 'URT::OtherChild', table_name => 'OTHER_CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ other_child_value => { is => 'STRING' }, ], ), "Created class for Other Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values (1, 'Bob', 'URT::Parent'))), "insert a parent object"); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values ( 2, 'Fred', 'URT::Child'))), "Insert part 1 of a child object"); ok($dbh->do(q(insert into child_table (child_id, child_value) values ( 2, 'stuff'))), "Insert part 2 of a child object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return $obj->the_type_name; } 37_caching_with_in_clause.t100664023532023421 1217012544604517 20206 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 61; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); my $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(name => [1,2,3,4,5]); is(scalar(@o), 5, 'get() returned the correct number of items with an in clause'); is($load_count, 5, 'loaded 5 objects'); is($query_count, 1, '1 query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [1,2,3,4,5]); is(scalar(@o), 5, 'get() returned the correct number of items with the same in clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [2,3,4]); is(scalar(@o), 3, 'get() returned the correct number of items with a subset in clause'); is($load_count, 0, 'loaded 0 new objects'); #is($query_count, 0, 'no query was generated (known broken)'); foreach my $id ( 1 .. 5 ) { $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => $id); is(scalar(@o), 1, 'get() returned 1 item with a single id'); is($load_count, 0, 'no new objects were loaded'); is($query_count, 0, 'no new queries were done'); } $load_count = 0; $query_count = 0; # Note that it's probably not worth it for the query system to remove 4 and 5 # before it constructs the SQL query @o = URT::Parent->get(name => [4,5,6,7]); is(scalar(@o), 4, 'get() returned the correct number of items with another in clause'); is($load_count, 2, '2 new objects were loaded'); is($query_count, 1, '1 new query was done'); # FIXME - subscriptions for 'query' doesn't pass along the SQL to the callback #ok($query_text !~ m/4,5,6,7/, q(Generated query does not mention "('4','5','6','7')")); #ok($query_text =~ m/6,7/, q(Generated query does mention "('6','7')")); $load_count = 0; $query_count = 0; my $iter = URT::Parent->create_iterator(name => [5,7,2,99,102], is_cool => 1); ok($iter, 'Created iterator with an in-clause'); ok($iter->next, 'Pull an object off the iterator'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $iter = undef; $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [5,7,2,99,102], is_cool => 1); is(scalar(@o), 3, 'get() returned the correct number of items with in clause containing some non-matching values'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 102, is_cool => 1,); is(scalar(@o), 0, 'get() correctly returns nothing for a non-matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 99, is_cool => 1,); is(scalar(@o), 0, 'get() correctly returns nothing for another non-matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 5); is(scalar(@o), 1, 'got one object by name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 99); is(scalar(@o), 1, 'There was one with name 99'); is($load_count, 1, 'loaded 0 new objects'); is($query_count, 1, 'no query was generated'); unlink(URT::DataSource::SomeSQLite->server); # Remove the DB file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, is_cool => { is => 'NUMBER' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Parent"); my $sth = $dbh->prepare('insert into parent_table (parent_id, name, is_cool) values (?,?,?)'); ok($sth,'insert statement prepared'); foreach my $n ( 1 .. 10 ) { ok($sth->execute($n,$n,1), "inserted parent ID $n"); } $sth->execute(99,99,0); # item 99 is not cool } 37b_caching_with_in_clause.t100664023532023421 440712544604517 20334 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); ok($dbh->do('create table thing ( thing_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, color varchar)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Thing', table_name => 'thing', id_by => [ 'thing_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, is_cool => { is => 'NUMBER' }, color => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Thing"); my $sth = $dbh->prepare('insert into thing (thing_id, name, is_cool, color) values (?,?,?,?)'); ok($sth,'insert statement prepared'); foreach my $n ( 1 .. 10 ) { ok($sth->execute($n,$n,1,'green'), "inserted thing ID $n"); } $sth->execute(99,99,0,'white'); # item 99 is not cool my $load_count = 0; ok(URT::Thing->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Thing->get(name => [5,7,2,99,102], is_cool => 1); is(scalar(@o), 3, 'get() returned the correct number of items with in clause containing some non-matching values'); is($load_count, 3, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $load_count = 0; $query_count = 0; @o = URT::Thing->get(name => 5, is_cool => 1, color => 'green'); is(scalar(@o), 1, 'get() correctly returns object matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); 38_join_across_data_sources.t100664023532023421 2201512544604517 20602 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 50; # FIXME - This tests the simple case of a single indirect property. # Need to add a test for a doubly-indirect property crossing 2 data # sources, and a test where the numeric order of things is differen # than the alphabetic order use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $tmp_path = "/tmp/ur_testsuite$$"; ok(mkdir($tmp_path), "mkdir temp dir"); our $DB_FILE_1 = "$tmp_path/ur_testsuite_db1_$$.sqlite"; our $DB_FILE_2 = "$tmp_path/ur_testsuite_db2_$$.sqlite"; END { &clean_tmp_dir($tmp_path); } &create_data_sources($tmp_path); &populate_databases($tmp_path); &create_test_classes($tmp_path); # Set up subscriptions to count queries and loads my($db1_query_count, $primary_load_count, $db2_query_count, $related_load_count); sub reset_counts { ($db1_query_count, $primary_load_count, $db2_query_count, $related_load_count) = (0,0,0,0); } ok(URT::38Primary->create_subscription( method => 'load', callback => sub {$primary_load_count++}), 'Created a subscription for URT::38Primary load'); ok(URT::38Related->create_subscription( method => 'load', callback => sub {$related_load_count++}), 'Created a subscription for URT::38Related load'); ok(URT::DataSource::SomeSQLite1->create_subscription( method => 'query', callback => sub {$db1_query_count++}), 'Created a subscription for SomeSQLite1 query'); ok(URT::DataSource::SomeSQLite2->create_subscription( method => 'query', callback => sub {$db2_query_count++}), 'Created a subscription for SomeSQLite2 query'); &reset_counts(); my @o = URT::38Primary->get(related_value => '1'); is(scalar(@o), 1, "contained_value => 1 returns one Primary object"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 1, "Loaded 1 Related object"); &reset_counts(); @o = URT::38Primary->get(primary_value => 'Two', related_value => '2'); is(scalar(@o), 1, "container_value => 'Two',contained_value=>2 returns one Primary object"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 1, "Loaded 1 Related object"); &reset_counts(); @o = URT::38Primary->get(related_value => '2'); is(scalar(@o), 2, "contained_value => 2 returns two Primary objects"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); # FIXME - This next one should really be 0, as the resulting query against db2 is exactly the same as # the prior get() above. The problem is that the cross-datasource join logic is # functioning at the database level, not the object level. So there's no good way of # knowing that we've already done that query. is($db2_query_count, 1, "Correctly didn't query db 2 (same as previous query)"); is($related_load_count, 0, "Correctly loaded 0 Related objects (they're cached)"); &reset_counts(); @o = URT::38Primary->get(related_value => '3'); is(scalar(@o), 0, "contained_value => 3 correctly returns no Primary objects"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); # Note - it kind of doesn't make sense that we do a query against db2, and that query does # match one item in there. UR doesn't go ahead and load it because the query against the # primary DB returns no rows, so there's nothing to 'join' against, and no rows from db2's # query are fetched is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "Correctly loaded 0 Related object"); &reset_counts(); @o = URT::38Primary->get(related_value => '4'); is(scalar(@o), 0, "contained_value => 4 correctly returns no Primary objects"); # Note - same thing here, the primary query fetches 1 row, but doesn't successfully # join to any rows in the secondary query, so no objects get loaded. is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "correctly loaded 0 Related objects"); &reset_counts(); @o = URT::38Related->get(related_value => 2, primary_values => 'Two'); is(scalar(@o), 1, 'URT::Related->get(primary_value => 2) returned 1 object'); # This actually ends up being 4 because of the way the Indexes get created. Don't think it's # useful to test it #is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "correctly loaded 0 Related objects"); sub create_data_sources { IO::File->new($DB_FILE_1, 'w')->close(); class URT::DataSource::SomeSQLite1 { is => 'UR::DataSource::SQLite', }; sub URT::DataSource::SomeSQLite1::server { $DB_FILE_1 }; IO::File->new($DB_FILE_2, 'w')->close(); class URT::DataSource::SomeSQLite2 { is => 'UR::DataSource::SQLite', }; sub URT::DataSource::SomeSQLite2::server { $DB_FILE_2 }; } sub create_test_classes { return; my $tmp_path = shift; # We have to write them out as files instead of calling UR::Object::Type->define() # because each class refers to the other unshift(@INC, $tmp_path); mkdir("$tmp_path/URT") || die "Can't create dir $tmp_path/URT"; my $f = IO::File->new("$tmp_path/URT/Related.pm",'>'); $f->print(q( use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::Object::Type->define( class_name => 'URT::Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { vis => 'primary_object', to => 'primary_value', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite2', table_name => 'related', ) 1; )); $f->close(); $f = IO::File->new("$tmp_path/URT/Primary.pm",'>'); $f->print(q( use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::Object::Type->define( class_name => 'URT::Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, related_id => { is => 'Integer'}, related_object => { is => 'URT::Related', id_by => 'related_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], data_source => 'URT::DataSource::SomeSQLite1', table_name => 'primary_table', ); 1; )); $f->close(); } sub populate_databases { my $dbh = URT::DataSource::SomeSQLite1->get_default_handle(); ok($dbh, 'Got db handle for URT::DataSource::SomeSQLite1'); ok($dbh->do("create table primary_table (primary_id integer PRIMARY KEY, primary_value varchar, rel_id integer)"), "create primary table"); # This one will match one item in related ok($dbh->do("insert into primary_table values (1, 'One', 1)"), "insert row 1 into primary"); # these two things will match one in related ok($dbh->do("insert into primary_table values (2, 'Two', 2)"), "insert row 2 into primary"); ok($dbh->do("insert into primary_table values (3, 'Three', 2)"), "insert row 3 into primary"); # Nothing here matches related's 3 # This will match nothing in related ok($dbh->do("insert into primary_table values (4, 'Four', 4)"), "insert row 4 into primary"); ok($dbh->commit(), "Commit SomeSQLite1 DB"); $dbh = URT::DataSource::SomeSQLite2->get_default_handle(); ok($dbh, 'Got db handle for URT::DataSource::SomeSQLite2'); ok($dbh->do("create table related (related_id integer PRIMARY KEY, related_value varchar)"), "crate related table"); ok($dbh->do("insert into related values (1, '1')"), "insert row 1 into related"); ok($dbh->do("insert into related values (2, '2')"), "insert row 2 into related"); ok($dbh->do("insert into related values (3, '3')"), "insert row 4 into related"); ok($dbh->commit(), "Commit SomeSQLite2 DB"); } sub clean_tmp_dir { my $tmp_dir = shift; my $dbh = URT::DataSource::SomeSQLite1->get_default_handle(); $dbh->disconnect(); $dbh = URT::DataSource::SomeSQLite2->get_default_handle(); $dbh->disconnect(); #diag("Cleanup tmp dir"); # These _should_ be the only files in there... ok(unlink($DB_FILE_1), 'Remove sqlite DB 1'); ok(unlink($DB_FILE_2), 'Remove sqlite DB 2'); ok(rmdir($tmp_dir), "Remove tmp dir $tmp_dir"); } 39_has_many.t100664023532023421 1345012544604517 15340 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 24; class Animal { has => [ fur => { is => 'Text' }, # Two an indirect properties # referencing a single value # via another object # through a has-many # ..and they're writable. # one is to a regular property limbs => { is => 'Animal::Limb', reverse_as => 'animal', is_mutable => 1, is_many => 1 }, foreleg_flexibility_score => { via => 'limbs', where => [ number => 1 ], to => 'flexibility_score', is_mutable => 1, }, # one is "to" an id property, notes => { is => 'Animal::Note', reverse_as => 'animal', is_mutable => 1, is_many => 1 }, primary_note_text => { via => 'notes', where => [ type => 'primary' ], to => 'text', is_mutable => 1 }, eyes => { is => 'Animal::Eye', reverse_as => 'animal', is_many => 1 }, antlers => { is => 'Animal::Antler', reverse_as => 'animal', is_many => 1 }, ], }; class Animal::Limb { id_by => [ animal => { is => 'Animal', id_by => 'animal_id' }, number => { is => 'Number' }, ], has => [ flexibility_score => { is => 'Number', is_optional => 1 }, ] }; class Animal::Note { id_by => [ animal => { is => 'Animal', id_by => 'animal_id' }, type => { is => 'Text' }, text => { is => 'Text' }, ] }; class Animal::Eye { has => [ animal => { is => 'Animal', id_by => 'animal_id' }, color => { is => 'String' }, ], }; class Animal::Antler { has => [ animal => { is => 'Animal', id_by => 'animal_id' }, pointiness => { is => 'Number' }, ], }; subtest 'accessor names' => sub { plan tests => 9; my $animal_meta = Animal->__meta__; is($animal_meta->singular_accessor_name_for_is_many_accessor('limbs'), 'limb', 'Singular name for limbs'); is($animal_meta->iterator_accessor_name_for_is_many_accessor('limbs'), 'limb_iterator', 'Iterator name for limbs'); is($animal_meta->set_accessor_name_for_is_many_accessor('limbs'), 'limb_set', 'Set name for limbs'); is($animal_meta->rule_accessor_name_for_is_many_accessor('limbs'), '__limb_rule', 'Rule name for limbs'); is($animal_meta->arrayref_accessor_name_for_is_many_accessor('limbs'), 'limb_arrayref', 'Arrayref name for limbs'); is($animal_meta->adder_name_for_is_many_accessor('limbs'), 'add_limb', 'Adder name for limbs'); is($animal_meta->remover_name_for_is_many_accessor('limbs'), 'remove_limb', 'Remover name for limbs'); ok(! $animal_meta->singular_accessor_name_for_is_many_accessor('fur'), 'Fur has no singular name'); ok(! $animal_meta->singular_accessor_name_for_is_many_accessor('nonsense'), 'Non-existent property has no singular name'); }; # make an example object my $a = Animal->create(); ok($a, 'new animal'); # add parts the hard way my $i1 = $a->add_limb(number => 1); ok ($i1, 'has one foot.'); my $i2 = $a->add_limb(number => 2); ok ($i2, 'has two feet!'); # make another, and add them in a slightly easier way my $a2 = Animal->create( limbs => [ { number => 1, flexibility_score => 11 }, { number => 2, flexibility_score => 22 }, { number => 3, flexibility_score => 33 }, { number => 4, flexibility_score => 44 }, ], fur => "fluffy", ); ok($a2, 'yet another animal'); my @i = $a2->limbs(); is(scalar(@i),4, 'expected 4 feet!'); # make a third object, and add them the easiest way my $a3 = Animal->create( limbs => [1,2,3,4], fur => "fluffy", ); ok($a3, 'more animals'); my @i2= $a3->limbs(); is(scalar(@i2),4, '4 feet again, the easy way'); # indirect access.. my $note1 = $a3->add_note(type => 'primary', text => "note1"); ok($note1, "made a note"); my $note2 = $a3->add_note(type => 'secondary', text => "note2"); ok($note2, "made another note"); my $t = $a3->primary_note_text("note1b"); is($t,"note1b", "set a remote partial-id-value through the indirect accessor"); $t = $a3->primary_note_text(); is($t,"note1b","got back the partial-id-value through the indirect accessor"); my $s = $a3->foreleg_flexibility_score(100); is($s,100,"set a remote non-id value through the indirect accessor"); $s = $a3->foreleg_flexibility_score(); is($s,100,"got back the non-id value through the indirect accessor"); # Give animal 3 two eyes of different colors # We're avoiding the add_eye method so the rule/template captured by the # method's closure isn't pre-created when we use the filterable accessor Animal::Eye->create(animal => $a3, color => 'blue'); Animal::Eye->create(animal => $a3, color => 'green'); my $eye = $a3->eye(color => 'green'); ok($eye, 'Got an eye via the filterable accessor'); is($eye->color, 'green', 'It is the correct eye'); $eye = $a3->eye(color => 'blue'); ok($eye, 'Got an eye via the filterable accessor'); is($eye->color, 'blue', 'It is the correct eye'); $eye = $a3->eye(color => 'tractor'); ok(! $eye, 'Correctly found no eye via the filterable accessor'); # Do it again with the missing thing first # and use the plural accessor to test that one out too Animal::Antler->create(animal => $a3, pointiness => 1); Animal::Antler->create(animal => $a3, pointiness => 2); my $antler = $a3->antlers(pointiness => 100); ok(! $antler, 'Correctly found no antler via the filterable accessor'); $antler = $a3->antlers(pointiness => 1); ok($antler, 'Got an antler via the filterable accessor'); is($antler->pointiness, 1, 'It is the correct antler'); $antler = $a3->antlers(pointiness => 2); ok($antler, 'Got an antler via the filterable accessor'); is($antler->pointiness, 2, 'It is the correct antler'); 39b_has_many.t100664023532023421 354712544604517 15470 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t# This tests that the add/remove singular accessors are not overridden if the # package defines them but instead installs __add/__remove accessors similar to # what is done with singular properties. use strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; my %ran_package_method; my $class_name = setup(\%ran_package_method); my $roster = $class_name->create(); $roster->add_member('Bob'); is_deeply([$roster->members], ['Bob'], 'added Bob'); ok($ran_package_method{add_member}, qq(ran the package add_member)); $roster->remove_member('Bob'); is_deeply([$roster->members], [], 'removed Bob'); ok($ran_package_method{remove_member}, qq(ran the package remove_member)); sub setup { my $ran_package_method = shift; my $class_name = 'Roster'; for my $type (qw(add remove)) { my $singular_accessor_name = $type . '_member'; if ($class_name->can($singular_accessor_name)) { die qq($class_name shouldn't be able to $singular_accessor_name yet); } my $ur_singular_accessor_name = '__' . $singular_accessor_name; if ($class_name->can($ur_singular_accessor_name)) { die qq($class_name shouldn't be able to $ur_singular_accessor_name yet); } no strict 'refs'; *{ $class_name . '::' . $singular_accessor_name } = sub { my $self = shift; $ran_package_method->{$singular_accessor_name} = 1; $self->$ur_singular_accessor_name(@_); }; use strict 'refs'; } my $class = UR::Object::Type->__define__( class_name => $class_name, has => [ members => { is => 'Text', is_many => 1, }, ], ); return $class->class_name; } 39c_has_many.t100664023532023421 473312544604517 15467 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t use strict; use warnings; use Test::More tests => 2; use Test::Deep qw(cmp_bag); use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; my $class = 'URT::Person1'; UR::Object::Type->define( class_name => $class, has => [ name => { is => 'Text', }, nicknames => { is => 'Text', is_many => 1, }, ], ); subtest $class => sub { run_tests($class, 1) }; $class = 'URT::Person2'; UR::Object::Type->define( class_name => $class, id_by => [ id => { is => 'Text', }, ], has => [ name => { is => 'Text', }, nickname_objects => { is => 'URT::Nickname', reverse_as => 'person', is_mutable => 1, is_many => 1, }, nicknames => { is => 'Text', via => 'nickname_objects', to => 'name', is_many => 1, is_mutable => 1, }, ], ); UR::Object::Type->define( class_name => 'URT::Nickname', id_by => [ person_id => { is => 'Text', }, name => { is => 'Text', }, ], has => [ person => { is => $class, id_by => 'person_id', }, ], ); subtest $class => sub { run_tests($class, 0) }; sub run_tests { my $class = shift; my $test_updates = shift; if ($test_updates) { plan tests => 4; } else { plan tests => 2; } my $tx = UR::Context::Transaction->begin(); my $nickname = 'Alyosha'; my $person = $class->create(name => 'Alexei', nicknames => $nickname); cmp_bag([$person->nicknames], [$nickname], 'set (and retrieved) a single nickname'); if($test_updates) { $nickname = 'Alex'; $person->nicknames($nickname); cmp_bag([$person->nicknames], [$nickname], 'updated (and retrieved) a single nickname'); } my @nicknames = qw(Rose Anna Roseanne Annie); my $person2 = $class->create(name => 'Roseanna', nicknames => \@nicknames); cmp_bag([$person2->nicknames], \@nicknames, 'set (and retrieved) several nicknames'); if($test_updates) { @nicknames = qw(Rosy Anne); $person2->nicknames(\@nicknames); cmp_bag([$person2->nicknames], \@nicknames, 'updated (and retrieved) several nicknames correctly'); } $tx->rollback(); } 39c_singular_reverse_as.t100664023532023421 730612544604517 17731 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use Test::More tests => 2; use Test::Fatal qw(exception); use Test::Deep qw(cmp_bag); use UR; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do(q(create table team (id integer PRIMARY KEY, name text))); $dbh->do(q(create table member (id integer PRIMARY KEY, name text, team_id text, role text))); $dbh->do(q(create table admin (id integer PRIMARY KEY, name text, team_id text))); $dbh->do(q(insert into team values (1, 'Three Stooges'))); $dbh->do(q(insert into member values (1, 'Larry', 1, 'member'))); $dbh->do(q(insert into member values (2, 'Curly', 1, 'member'))); $dbh->do(q(insert into member values (3, 'Moe', 1, 'admin'))); $dbh->do(q(insert into admin values (1, 'Moe', 1))); $dbh->do(q(insert into team values (2, "Who's the Boss?"))); $dbh->do(q(insert into member values (4, 'Tony Micelli', 2, 'admin'))); $dbh->do(q(insert into member values (5, 'Angela Bower', 2, 'admin'))); $dbh->do(q(insert into member values (6, 'Samantha Micelli', 2, 'member'))); $dbh->do(q(insert into member values (7, 'Jonathan Bower', 2, 'member'))); $dbh->do(q(insert into member values (8, 'Mona Robinson', 2, 'member'))); $dbh->do(q(insert into admin values (2, 'Tony Micelli', 2))); $dbh->do(q(insert into admin values (3, 'Angela Bower', 2))); $dbh->commit(); UR::Object::Type->define( class_name => 'Team', id_by => 'id', has => [ name => { is => 'Text' }, members => { is => 'Member', reverse_as => 'team', is_many => 1 }, admin => { is => 'Member', reverse_as => 'team', is_many => 0, where => ['role' => 'admin'] }, alt_admin => { is => 'Admin', reverse_as => 'team', is_many => 0 }, ], table_name => 'team', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Member', id_by => 'id', has => [ name => { is => 'Text' }, team => { is => 'Team', id_by => 'team_id' }, role => { is => 'Text', valid_values => [qw(admin member)] }, ], table_name => 'member', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Admin', id_by => 'id', has => [ name => { is => 'Text' }, team => { is => 'Team', id_by => 'team_id' }, ], table_name => 'admin', data_source => 'URT::DataSource::SomeSQLite', ); subtest 'Three Stooges' => sub { plan tests => 9; my $team = Team->get(name => 'Three Stooges'); my $larry = Member->get(name => 'Larry'); my $curly = Member->get(name => 'Curly'); my $moe = Member->get(name => 'Moe'); cmp_bag([$team->members], [$larry, $curly, $moe], 'got members'); is($team->admin, $moe, 'got admin'); is($team->alt_admin->name, 'Moe', 'got alt_admin'); is(Member->get(team => $team, role => 'admin'), $moe, 'got admin member via a team'); is(Team->get(admin => $moe), $team, 'got team via admin'); is(Team->get('admin.name' => $moe->name), $team, 'got team via admin.name'); is(Admin->get(team => $team)->name, 'Moe', 'got alt_admin via a team'); is(Team->get(alt_admin => $moe), $team, 'got team via alt_admin'); is(Team->get('alt_admin.name' => $moe->name), $team, 'got team via alt_admin.name'); }; subtest q(Who's the Boss?) => sub { plan tests => 3; my $team = Team->get(name => q(Who's the Boss?)); is(scalar(() = $team->members), 5, 'got five members'); ok(exception { $team->admin }, 'got an exception when trying to get the admin'); ok(exception { $team->alt_admin }, 'got an exception when trying to get the alt_admin'); }; 39d_composite_id_by.t100664023532023421 301212544604517 17026 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 4; # Test that an id-by property using a composite ID for its value can refer # to a class with multuple ID properties. # # Normally this isn't a problem since most classes compose composite IDs # with join(). Passing in a single, composite ID to the composer returns # back the same composite value. Some classes have custom ID compositers # (such as UR::Value::JSON) that require multiple values to be passed in # to their ID compositers, and return a garbage value if a single, already- # composite ID is passed in. class Person { id_by => ['first_name','last_name'], has => [ things => { is => 'Thing', reverse_as => 'owner', is_many => 1 }, ], }; Person->__meta__->{'get_composite_id_resolver'} = sub { return join(':', map { $_ => shift } qw(first_name last_name)); }; class Thing { has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; my $person = Person->create(first_name => 'Bob', last_name => 'Smith'); ok($person, 'Create Person with multiple ID properties'); my(@things) = map { Thing->create(owner_id => $person->id) } (1..2); is(scalar(@things), 2, 'Create 2 Things with owner_id'); is($things[0]->owner, $person, "Thing's owner object is the Person object"); is_deeply([ sort { $a->id cmp $b->id } $person->things], \@things, 'Got 2 Things owned by Person'); 40_has_many_direct.t100664023532023421 534212544604517 16643 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 20; # make sure the INDIRECT stuff still works class Order { has => [], has_many => [ lines => { is => "Line" }, line_numbers => { via => "lines", to => "line_num" }, ] }; class Line { id_by => [ order => { is => "Order", id_by => "order_id" }, line_num => { is => "Number" }, ], }; #print Data::Dumper::Dumper(Line->__meta__); my $o = Order->create( lines => [ 1, 2, 17 ] ); my @lines = $o->lines; my @line_nums = sort $o->line_numbers(); is("@line_nums", "1 17 2", "has-many with INDIRECT relationships still works correctly, now trying the new stuff..."); class FileList { has_many => [ files => { is => 'FileName' }, ] }; #print Data::Dumper::Dumper(MyCommand->__meta__); #my $m = MyCommand->__meta__->property_meta_for_name("files"); #print Data::Dumper::Dumper($m); my $list1 = FileList->create( files => ['a','b','c'] ); ok($list1, "made new object"); my @f = $list1->files(); is(scalar(@f),3,"got back expected value count"); is("@f", "a b c", "got back expected values: @f"); my $new = $list1->add_file("d"); is($new,"d","added a new value"); @f = $list1->files(); is(scalar(@f),4,"got expected value count"); is("@f","a b c d", "got expected values: '@f'"); my $list2 = FileList->create(); my $fx = $list2->file("xxx"); is($fx,undef,"correctly failed to find a made-up value"); my $f1 = $list2->add_file("aaa"); is($f1,"aaa","added a new value, retval is correct"); my $f1r = $list2->file("aaa"); is($f1r,$f1,"got it back through single accessor"); @f = $list2->files; is(scalar(@f),1,"list has expected count"); is($f[0],$f1,"items are correct"); my $f2 = $list2->add_file("bbb"); my $f2r = $list2->file("bbb"); is($f2,$f2r,"added another file and got it back correctly: $f2"); @f = $list2->files; is(scalar(@f),2,"list has expected count"); is("@f","aaa bbb","items are correct"); my (@actual,@expected); my $f3 = FileList->create(files => [qw/4 1 2 5 3/]); @expected = (4,1,2,5,3); @actual = $f3->files; is("@expected","@actual","created object has expected list"); $f3->add_file("22"); @expected = (4,1,2,5,3,22); @actual = $f3->files; is("@expected","@actual","correct after adding an item"); $f3->remove_file("5"); @expected = (4,1,2,3,22); @actual = $f3->files; is("@expected","@actual","correct after removing an item"); $a = [qw/11 22 33/]; $f3->files($a); @expected = (11,22,33); @actual = $f3->files; is("@expected","@actual","correct after setting an item"); push @$a,"44"; is("@expected","@actual","changing the arrayref after setting it has no effect, as expected"); 41_rpc_basic.t100664023532023421 1442512544604517 15462 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; plan tests => 40; use File::Basename; BEGIN { use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; } use URT; use IO::Socket; ok(UR::Object::Type->define( class_name => 'URT::RPC::Thingy', is => 'UR::Service::RPC::Executer'), 'Created class for RPC executor'); my($to_server,$to_client) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC); ok($to_server, 'Created socket'); ok($to_client, 'Created socket'); my $rpc_executer = URT::RPC::Thingy->create(fh => $to_client); my $rpc_server = UR::Service::RPC::Server->create(); ok($rpc_server, 'Created an RPC server'); ok($rpc_server->add_executer($rpc_executer), 'Added the executer to the server'); #my $rpc_client = UR::Service::RPC::Client->create(fh => $to_server); #ok($rpc_client, 'Created an RPC client'); my $count = $rpc_server->loop(1); is($count, 0, 'RPC server ran the event loop and correctly processed 0 events'); my $retval; my @join_args = ('one','two','three','four'); my $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); ok($msg, 'Created an RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); do { local *STDERR; no warnings; $count = $rpc_server->loop(1); use warnings; }; is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); is($URT::RPC::Thingy::join_called, 1, 'RPC server called the correct method'); my $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my $expected_return_value = join('-',@join_args); my @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], $expected_return_value, 'Response return value is correct'); is($resp->exception, undef, 'Response correctly has no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'illegal', params => \@join_args, 'wantarray' => 0, ); ok($msg, 'Created another RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); is($URT::RPC::Thingy::exception, 1, 'RPC server correctly rejected the method call'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); is($resp->return_value, undef, 'Response return value is correctly empty'); is($resp->exception, 'Not allowed', 'Response excpetion is correctly set'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'some_undefined_function', params => [], 'wantarray' => 0, ); ok($msg, 'Created third RPC message encoding an undefined function call'); ok($msg->send($to_server), 'Sent RPC message from client'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); ok($resp->exception =~ m/(Can't locate object method|Undefined sub).*some_undefined_function/, 'Response excpetion correctly reflects calling an undefined function'); my $string = 'a string with some words'; my $pattern = '(\w+) (\w+) (\w+)'; my $regex = qr($pattern); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 0, ); ok($msg, 'Created RPC message for match in scalar context'); ok($msg->send($to_server), 'Sent RPC message to server'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], 1, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 1, ); ok($msg, 'Created RPC message for match in list context'); ok($msg->send($to_server), 'Sent RPC message to server'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my @expected_return_value = qw(a string with); @return_values = $resp->return_value_list; is_deeply(\@return_values, \@expected_return_value, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); # END of the main script package URT::RPC::Thingy; sub authenticate { my($self,$msg) = @_; if ($msg->method_name eq 'illegal') { $URT::RPC::Thingy::exception++; $msg->exception('Not allowed'); return; } else { return 1; } } sub join { my($joiner,@args) = @_; $URT::RPC::Thingy::join_called++; my $string = join($joiner, @args); return $string; } # A thing that will return different values in scalar and list context sub match { my($string, $regex) = @_; # my $pattern = qr($pattern); return $string =~ $regex; } 42_rpc_between_processes.t100664023532023421 1725312544604517 20123 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; BEGIN { use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; } use URT; use above "UR"; use Test::More;# skip_all => "fork() causes intermittent failure in TAP output"; use Test::Fork; use IO::Socket; # TCP sockets are used when running separate processes for # debugging the test # Let the system pick a socket for us, and then close it. We'll use ReuseAddr # when we re-open it our $PORT; { my $s = IO::Socket::INET->new(Listen => 1, Proto => 'tcp'); $PORT = $s->sockport(); } STDOUT->autoflush(1); STDERR->autoflush(1); my($to_server,$to_client); if ($ARGV[0] and $ARGV[0] ne '--parent' and $ARGV[0] ne '--child') { ($to_server, $to_client) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC); } elsif ($ARGV[0] and $ARGV[0] eq '--child') { # This is for debugging the test case. # It will start up just the child part note('Starting up the child portion'); &Child(); exit(0); } plan tests => 35; my $pid; if ($ARGV[0] and $ARGV[0] eq '--parent') { 1; # do nothing special #} elsif (! ($pid = fork())) { # # child - server # &Child(); # exit(0); #} } else { $pid = fork_ok(6, \&Child); } END { unless ($ARGV[0]) { if ($pid) { note("killing child PID $pid\n"); kill 'TERM', $pid; } elsif (getppid() != 1) { note("Child is exiting early... killing parent"); kill 'TERM', getppid(); } } } # parent #plan tests => 28; unless ($to_server) { sleep(1); # Give the child a change to get started $to_server = IO::Socket::INET->new(PeerHost => '127.0.0.1', PeerPort => $PORT); } $to_client && $to_client->close(); ok($to_server, 'Created a socket connected to the child process ' . $!); my @join_args = ('one','two','three','four'); my $msg = UR::Service::RPC::Message->create( #target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); ok($msg, 'Created an RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); my $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my $expected_return_value = join('-',@join_args); my @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], $expected_return_value, 'Response return value is correct'); is($resp->exception, undef, 'Response correctly has no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'illegal', params => \@join_args, 'wantarray' => 0, ); ok($msg, 'Created another RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); is($resp->exception, 'Not allowed', 'Response excpetion is correctly set'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'some_undefined_function', params => [], 'wantarray' => 0, ); ok($msg, 'Created third RPC message encoding an undefined function call'); ok($msg->send($to_server), 'Sent RPC message from client'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); ok($resp->exception =~ m/(Can't locate object method|Undefined sub).*some_undefined_function/, 'Response excpetion correctly reflects calling an undefined function'); my $string = 'a string with some words'; my $pattern = '(\w+) (\w+) (\w+)'; my $regex = qr($pattern); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 0, ); ok($msg, 'Created RPC message for match in scalar context'); ok($msg->send($to_server), 'Sent RPC message to server'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single value'); is($return_values[0], 1, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 1, ); ok($msg, 'Created RPC message for match in list context'); ok($msg->send($to_server), 'Sent RPC message to server'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my @expected_return_value = qw(a string with); is_deeply($resp->return_value_arrayref, \@expected_return_value, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); sub Child { #plan tests => 6; ok(UR::Object::Type->define( class_name => 'URT::RPC::Listener', is => 'UR::Service::RPC::TcpConnectionListener'), 'Created class for RPC socket Listener'); ok(UR::Object::Type->define( class_name => 'URT::RPC::Thingy', is => 'UR::Service::RPC::Executer'), 'Created class for RPC executor'); unless ($to_client) { $to_client = IO::Socket::INET->new(LocalPort => $PORT, Proto => 'tcp', Listen => 5, Reuse => 1); } $to_server && $to_server->close(); ok($to_client, 'Created TCP listen socket'); my $listen_executer = URT::RPC::Listener->create(fh => $to_client); ok($listen_executer, 'Created RPC executer for the listen socket'); my $rpc_server = UR::Service::RPC::Server->create(); ok($rpc_server, 'Created an RPC server'); ok($rpc_server->add_executer($listen_executer), 'Added the listen executer to the server'); #$rpc_server->add_executer($listen_executer); note('Child process entering the event loop'); while(1) { $rpc_server->loop(undef); } } # END of the main script package URT::RPC::Listener; sub worker_class_name { 'URT::RPC::Thingy'; } package URT::RPC::Thingy; sub authenticate { my($self,$msg) = @_; if ($msg->method_name eq 'illegal') { #$URT::RPC::Thingy::exception++; $msg->exception('Not allowed'); return; } else { return 1; } } sub join { my($joiner,@args) = @_; #$URT::RPC::Thingy::join_called++; my $string = join($joiner, @args); return $string; } # A thing that will return different values in scalar and list context sub match { my($string, $regex) = @_; # my $pattern = qr($pattern); return $string =~ $regex; } 43_infer_values_from_rule.t100664023532023421 760612544604517 20256 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; plan tests => 27; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::DBI->no_commit(1); &create_test_data(); my($rule,$value,@values); my $context = UR::Context->get_current; $rule = UR::BoolExpr->resolve('URT::43Primary', primary_id => 1, primary_value => 'One'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'get a value directly in the rule'); $rule = UR::BoolExpr->resolve('URT::43Primary', rel_id => 1); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'infer a direct property with a rule also containing a different direct property'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, 1, 'infer an indirect property with a rule containing a direct property'); $rule = UR::BoolExpr->resolve('URT::43Primary', related_value => '1'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('rel_id', $rule); is($value, 1, 'infer a direct linking property with a rule containing an indirect property'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'infer a direct property with a rule containing an indirect property'); $rule = UR::BoolExpr->resolve('URT::43Primary', related_value => '2'); ok($rule, 'Create rule'); @values = $context->infer_property_value_from_rule('primary_id', $rule); @values = sort @values; is(scalar(@values), 2, 'inferring a direct property with a rule containing an indirect property matching 2 objects'); is($values[0], 2, 'matched first primary_id'); is($values[1], 3, 'matched second primary_id'); # This ends up returning '3' because there's a Related object with related_id => 3 # though there is no Primary object with a rel_id => 3 #$rule = UR::BoolExpr->resolve('URT::43Primary', rel_id => 3); #ok($rule, 'Create rule'); #$value = $context->infer_property_value_from_rule('related_value', $rule); #is($value, undef, 'infer an indirect property with a rule containing a direct property matching nothing correctly returns undef'); $rule = UR::BoolExpr->resolve('URT::43Related', related_id => 2); ok($rule, 'Create rule'); @values = $context->infer_property_value_from_rule('primary_values', $rule); @values = sort {$a cmp $b} @values; is(scalar(@values), 2, 'infer an indirect, reverse_as property with a rule containing a direct property'); is($values[0], 'Three', 'first inferred value was correct'); is($values[1], 'Two', 'first inferred value was correct'); $rule = UR::BoolExpr->resolve('URT::43Related', primary_values => 'One'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, '1', 'infer direct property with a rule containing an indirect, reverse_as property'); $rule = UR::BoolExpr->resolve('URT::43Related', primary_values => 'Two'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, '2', 'infer direct property with a rule containing an indirect, reverse_as property'); sub create_test_data { ok(URT::43Primary->create(primary_id => 1, primary_value => 'One',rel_id => 1), 'Create test object'); ok(URT::43Primary->create(primary_id => 2, primary_value => 'Two',rel_id => 2), 'Create test object'); ok(URT::43Primary->create(primary_id => 3, primary_value => 'Three',rel_id => 2), 'Create test object'); ok(URT::43Primary->create(primary_id => 4, primary_value => 'Four',rel_id => 4), 'Create test object'); ok(URT::43Related->create(related_id => 1, related_value => '1'), 'Create test object'); ok(URT::43Related->create(related_id => 2, related_value => '2'), 'Create test object'); ok(URT::43Related->create(related_id => 3, related_value => '3'), 'Create test object'); } 44_modulewriter.t100664023532023421 1472012544604517 16260 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse Test::More; use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; plan tests => 14; use_ok('UR::Object::Type::ModuleWriter'); eval { my $f = \&UR::Object::Type::_quoted_value; my @tests = ( [q(123), q(123)], [q(1.23), q(1.23)], [q(abc), q('abc')], [q(a'c), q(q(a'c))], ); for my $test (@tests) { my ($i, $e) = @$test; is($f->($i), $e, "_quoted_value matched: $i"); } }; eval { my $f = \&UR::Object::Type::_idx; my @tests = ( ['is', 0], ['foo', 2], ); for my $test (@tests) { my ($i, $e) = @$test; is($f->($i), $e, "_idx matched: $i"); } }; eval { my $f = \&UR::Object::Type::_sort_keys; my @i = qw(foo bar is baz); my @e = qw(is bar baz foo); my @o = $f->(@i); is_deeply(\@o, \@e, "_sort_keys matched: " . join(', ', @i)); }; eval { my $f = \&UR::Object::Type::_exclude_items; my @i = qw(foo bar baz qux); my @x = qw(foo baz); my @e = qw(bar qux); my @o = $f->(\@i, \@x); is_deeply(\@o, \@e, "_exclude_items matched: [" . join(', ', @i) . "], [" . join(', ', @x) . "]"); }; # First, make a couple of classes we can point to my $c = UR::Object::Type->define( class_name => 'URT::Related', id_by => [ related_id => { is => 'String' }, related_id2 => { is => 'String' }, ], has => [ related_value => { is => 'String'}, ], ); ok($c, 'Defined URT::Related class'); $c = UR::Object::Type->define( class_name => 'URT::Parent', type_has => [ some_type_meta => { is => 'ARRAY', is_optional => 1, }, ], id_by => [ parent_id => { is => 'String' }, ], has => [ parent_value => { is => 'String' }, ], ); ok($c, 'Defined URT::Parent class'); $c = UR::Object::Type->define( class_name => 'URT::Remote', id_by => [ remote_id => { is => 'Integer' }, ], has => [ # test_obj => { is => 'URT::TestClass', id_by => ['prop1','prop2','prop3'] }, something => { is => 'String' }, ], ); ok($c, 'Defined URT::Remote class'); # Make up a class definition with all the different kinds of properties we can think of... # FIXME - I'm not sure how the attributes_have and id_implied stuff is meant to work my $test_class_definition = q( is => 'URT::Parent', table_name => 'PARENT_TABLE', type_has => [ some_new_property => { is => 'Integer', is_optional => 1 }, ], attributes_have => [ meta_prop_a => { is => 'Boolean', is_optional => 1 }, meta_prop_b => { is => 'String' }, ], some_type_meta => [ "foo" ], subclassify_by => 'my_subclass_name', id_by => [ another_id => { is => 'String', doc => 'blahblah' }, related => { is => 'URT::Related', id_by => [ 'parent_id', 'related_id' ], doc => 'related', }, foobaz => { is => 'Integer' }, ], has => [ property_0 => { via => '__self__', to => 'property_a' }, property_a => { is => 'String', meta_prop_a => 1 }, property_b => { is => 'Integer', is_abstract => 1, meta_prop_b => 'metafoo', doc => q(property'b), }, calc_sql => { calculate_sql => q(to_upper(property_b)) }, some_enum => { is => 'Integer', column_name => 'SOME_ENUM', valid_values => [ 100, 200, 300 ], }, another_enum => { is => 'String', column_name => 'different_name', valid_values => [ "one", "two", "three", 3, "four" ], }, my_subclass_name => { is => 'Text', calculate_from => [ 'property_a', 'property_b' ], calculate => q("URT::TestClass"), }, subclass_by_prop => { is => 'String' }, subclass_by_id => { is => 'Integer' }, subclass_by_obj => { is => 'UR::Object', id_by => 'subclass_by_id', id_class_by => 'subclass_by_prop', }, ], has_many => [ property_cs => { is => 'String', is_optional => 1 }, remotes => { is => 'URT::Remote', reverse_as => 'testobj', where => [ something => { operator => 'like', value => '%match%' } ], }, set_remotes => { is => 'URT::Remote', reverse_as => 'testobj', is_mutable => 1, where => [ something => { operator => 'like', value => '%match%' } ], }, ], has_optional => [ property_d => { is => 'Number' }, calc_perl => { calculate_from => [ 'property_a', 'property_b' ], calculate => q($property_a . $property_b), }, another_related => { is => 'URT::Related', id_by => [ 'rel_id1', 'rel_id2' ], where => [ 'property_a like' => 'foo', property_b => [ "foo", "bar" ] ], is_many => 1, }, related_value => { is => 'StringSubclass', via => 'another_related', is_many => 1, }, related_value2 => { is => 'StringSubclass', via => 'another_related', to => 'related_value', is_mutable => 1, is_many => 0, }, ], schema_name => 'SomeFile', data_source => 'URT::DataSource::SomeFile', id_generator => 'the_sequence_seq', valid_signals => ['nonstandard1', 'something_else', 'third_thing'], doc => 'Hi there', ); my $orig_test_class = $test_class_definition; my $test_class_meta = eval "UR::Object::Type->define(class_name => 'URT::TestClass', $test_class_definition);"; ok($test_class_meta, 'Defined URT::TestClass class'); if ($@) { diag("Errors from class definition:\n$@"); exit(1); } my $string = $test_class_meta->resolve_class_description_perl(); my $orig_string = $string; # Normalize them by removing newlines, and multiple spaces $test_class_definition =~ s/\n//gm; $test_class_definition =~ s/\s+/ /gm; $string =~ s/\n//gm; $string =~ s/\s+/ /gm; my $diffcmd = 'diff -u'; if ($string ne $test_class_definition) { ok(0, 'Rewritten class definition matches original'); IO::File->new('>/tmp/old')->print($orig_test_class); IO::File->new('>/tmp/new')->print($orig_string); diag(qx($diffcmd /tmp/old /tmp/new)); } else { ok(1, 'Rewritten class definition matches original'); } 45_deleted_subclassed_objects_stay_deleted.t100664023532023421 361512544604517 23575 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 11; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); { my $o = URT::Parent->get(parent_id => 1); ok($o, 'Got an object'); isa_ok($o, 'URT::Parent'); isa_ok($o, 'URT::Child'); ok($o->delete, 'Object deleted ok'); } { my $o = URT::Parent->get(parent_id => 1); ok(! $o, 'get() with the deleted ID returns nothing'); } { my $o = URT::Parent->get(parent_id => 1); ok(! $o, 'get() with the deleted ID again returns nothing'); } unlink(URT::DataSource::SomeSQLite->server); # Remove the file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', is => [ 'URT::Parent' ], ), "Created class for Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name) values (1, 'Bob'))), "insert a parent object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return 'URT::Child'; } 45_rollback_deleted_object.t100664023532023421 1112312544604517 20336 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 60; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeSQLite; # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); # SQLite's rollback un-does the table creation, too, so we # have to re-create the table and object when no-commit is on # And this needs to be re-created each time the main Context rolls back # because subscription creation is a transactional action my $init_db = sub { $dbh->do('create table IF NOT EXISTS person ( person_id int NOT NULL PRIMARY KEY, name varchar)'); $dbh->do(q(delete from person)); $dbh->do(q(insert into person (person_id, name) values (1, 'Bob'))); }; $init_db->(); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'person', id_by => [ 'person_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Person"); my $o = URT::Person->get(person_id => 1); ok($o, 'Got an object'); { my $within = sub { ok($o->delete, 'Object deleted ok'); isa_ok($o, 'UR::DeletedRef'); ok(! URT::Person->get(person_id => 1), 'get() does not return the deleted object'); }; my $after = sub { isa_ok($o, 'URT::Person'); my $o2 = URT::Person->get(person_id => 1); ok($o2, 'get() returns the object again'); is($o2, $o, 'the returned object is the same reference as the original'); }; &try_in_sw_transaction($within, $after); &try_in_context_transaction($within,$after); } { my $within = sub { ok($o->delete, 'Delete the object'); isa_ok($o, 'UR::DeletedRef'); my $new_o = URT::Person->create(person_id => 1, name => 'Fred'); ok($new_o, 'Created a new Person with the same ID as the deleted one'); is($new_o, $o, 'They are the same reference'); # The IDs are the same, so they're the same thing isa_ok($new_o, 'URT::Person'); is($new_o->name, 'Fred', 'Name is the new object name'); }; my $after = sub { isa_ok($o, 'URT::Person'); my $o2 = URT::Person->get(person_id => 1); ok($o2, 'get() returns the object again'); is($o2, $o, 'the returned object is the same reference as the original'); is($o->name, 'Bob', 'Name is the original object name'); }; &try_in_sw_transaction($within, $after); &try_in_context_transaction($within,$after); } { # Doing this with the outer Context's transaction makes no sense # Just test in a software transaction my $trans1 = UR::Context::Transaction->begin(); ok($trans1, 'Started a software transaction'); ok($o->name('Fred'), 'Change object name to Fred'); my $trans2 = UR::Context::Transaction->begin(); ok($trans2,'Start an inner transaction'); ok($o->delete,'Delete the object'); isa_ok($o, 'UR::DeletedRef'); ok(! URT::Person->get(person_id => 1), 'get() does not return the deleted object'); ok($trans2->rollback, 'Rollback inner transaction'); isa_ok($o, 'URT::Person'); is($o->name, 'Fred', 'Object name is still Fred'); ok($trans1->rollback, 'Rollback outter transaction'); is($o->name, 'Bob', 'Object name is back to Bob'); } { # And this one makes no sense with a software transaction since # it needs to hit the DB ok(UR::DBI->no_commit(1), 'Turn on no-commit'); my $new_o = URT::Person->create(person_id => 2, name => 'Fred'); ok($new_o, 'Create a new Person'); ok(UR::Context->commit(),'Context commit'); ok($new_o->delete(),'Delete the new object'); isa_ok($new_o, 'UR::DeletedRef'); ok(UR::Context->rollback(),'Context rollback'); isa_ok($new_o, 'URT::Person'); is($new_o->name, 'Fred', 'The object name is Fred'); } #################################################################3 sub try_in_sw_transaction { my $within = shift; my $after = shift; my $trans = UR::Context::Transaction->begin(); ok($trans, 'Started a software transaction'); $within->(); ok($trans->rollback(), 'rollback the software transaction'); $after->(); } sub try_in_context_transaction { my $within = shift; my $after = shift; $within->(); ok(UR::Context->rollback(), 'rollback the context'); $init_db->(); $after->(); } 46_meta_property_relationships.t100664023532023421 2160512544604517 21376 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse Test::More; use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; plan tests => 56; ok( UR::Object::Type->define( class_name => 'URT::Related', id_by => ['rel_id_a', 'rel_id_b'], # purposefully make the complete definitions for the ID properties # in a different order. The real order should be whatever was in id_by has => [ rel_id_b => { is => 'Integer' }, related_value => { is => 'String' }, rel_id_a => { is => 'Integer' }, ], ), 'Define related class'); ok( UR::Object::Type->define( class_name => 'URT::Parent', id_by => [ parent_id => { is => 'Integer' } ], has => [ parent_value => { is => 'String' }, related_object => { is => 'URT::Related', id_by => ['rel_id_a', 'rel_id_b']}, related_value => { via => 'related_object', to => 'related_value' }, ] ), 'Define parent class'); ok( UR::Object::Type->define( class_name => 'URT::Child', is => 'URT::Parent', id_by => [ child_id => { is => 'Integer' } ], has => [ child_value => { is => 'String' }, ], ), 'Define child class'); my $parent_meta = URT::Parent->__meta__; ok($parent_meta, 'Parent class metadata'); my @props = $parent_meta->direct_id_property_metas(); is(scalar(@props), 1, 'Parent class has 1 ID property'); my @names = map { $_->property_name } @props; my @expected = qw(parent_id); is_deeply(\@names, \@expected, 'Property names match'); my $related_meta = URT::Related->__meta__; ok($related_meta, 'Related class metadata'); @props = $related_meta->direct_id_property_metas(); is(scalar(@props), 2, 'Related class has 2 ID properties'); @names = map { $_->property_name } @props; @expected = qw(rel_id_a rel_id_b); is_deeply(\@names, \@expected, 'Property names match'); my $prop = $related_meta->property_meta_for_name('rel_id_a'); # is_id actually returns "0 but true" for the first one is($prop->is_id + 0, 0, 'id position for Related property rel_id_a is 0'); $prop = $related_meta->property_meta_for_name('rel_id_b'); is($prop->is_id, 1, 'id position for Related property rel_id_b is 1'); $prop = $related_meta->property_meta_for_name('related_value'); is($prop->is_id, undef, 'id position for Related property rel_id_b is undef'); @props = $parent_meta->direct_property_metas(); is(scalar(@props), 6, 'Parent class has 6 direct properties with direct_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(parent_id parent_value rel_id_a rel_id_b related_object related_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $parent_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); $prop = $parent_meta->direct_property_meta(property_name => 'related_value'); ok($prop, 'singular property accessor works'); my $child_meta = URT::Child->__meta__; ok($child_meta, 'Child class metadata'); @props = $child_meta->direct_property_metas(); is(scalar(@props), 2, 'Child class has 2 direct properties'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $child_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); @props = $child_meta->all_property_metas(); is(scalar(@props), 9, 'Child class has 9 properties through all_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names,\@expected, 'Property names check out'); # properties() only returns properties with storage, not object accessors or the property named 'id' @props = $child_meta->properties(); is(scalar(@props), 9, 'Child class has 9 properties through properties()') or diag join(", ",$child_meta->property_names); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names,\@expected, 'Property names check out') or diag "@names\n@expected\n"; $prop = $child_meta->direct_property_meta(property_name => 'related_value'); ok(! $prop, "getting a property defined on parent class through child's direct_property_meta finds nothing"); $prop = $child_meta->property_meta_for_name('related_value'); ok($prop, "getting a property defined on parent class through child's property_meta_for_name works"); ok(UR::Object::Property->create( class_name => 'URT::Child', property_name => 'extra_property', data_type => 'String'), 'Created an extra property on Child class'); @props = $child_meta->properties(); is(scalar(@props), 10, 'Child class now has 10 properties()'); @names = map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out') or diag ("@names\n@expected\n"); @props = $child_meta->direct_property_metas(); is(scalar(@props), 3, 'Child class now has 3 direct_property_metas()'); @props = $child_meta->all_property_metas(); is(scalar(@props), 10, 'Child class now has 10 properties through all_property_names()'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out'); ok(UR::Object::Property->create( class_name => 'URT::Parent', property_name => 'parent_extra', data_type => 'String'), 'Created extra property on parent class'); @props = $parent_meta->direct_property_metas(); is(scalar(@props), 7, 'Parent class now has 7 direct properties with direct_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $parent_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); @props = $child_meta->properties(); is(scalar(@props), 11, 'Child class now has 11 properties()'); @names = map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out') or diag "@names\n@expected\n"; @props = $child_meta->all_property_metas(); is(scalar(@props), 11, 'Child class now has 11 properties through all_property_names()'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out'); @props = $parent_meta->property_meta_for_name('related_object'); is(scalar(@props), 1, 'Parent class has a property called related_object'); is($props[0]->property_name, 'related_object', 'Got the right property'); @props = $child_meta->property_meta_for_name('related_object'); is(scalar(@props), 1, 'Child class also has a property called related_object'); is($props[0]->property_name, 'related_object', 'Got the right property'); @props = $child_meta->property_meta_for_name('related_object.related_value'); is(scalar(@props), 2, 'Got 2 properties involved for related_object.related_value on the child class'); is($props[0]->class_name, 'URT::Parent', 'First property meta\'s class_name is correct'); is($props[0]->property_name, 'related_object', 'First property meta\'s property_name is correct'); is($props[1]->class_name, 'URT::Related', 'second class_name for that property is correct'); is($props[1]->property_name, 'related_value', 'second property_name is correct'); @props = $child_meta->property_meta_for_name('non_existent'); is(scalar(@props), 0, 'No property found for name \'non_existent\''); @props = $child_meta->property_meta_for_name('non_existent.also_non_existent'); is(scalar(@props), 0, 'No property found for name \'non_existent.also_non_existent\''); @props = $child_meta->property_meta_for_name('related_object.also_non_existent'); is(scalar(@props), 0, 'No property found for name \'related_object.also_non_existent\''); my @classes = $child_meta->parent_class_metas(); is(scalar(@classes), 1, 'Child class has 1 parent class'); @names = map { $_->class_name } @classes; @expected = qw( URT::Parent ); is_deeply(\@names, \@expected, 'parent class names check out'); @names = sort $child_meta->ancestry_class_names; is(scalar(@names), 2, 'Child class has 2 ancestry classes'); @expected = qw( UR::Object URT::Parent ); is_deeply(\@names, \@expected, 'Class names check out'); 47_indirect_is_many_accessor.t100664023532023421 554612544604517 20731 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 14; UR::Object::Type->define( class_name => 'URT::Param', id_by => [ thing_id => { is => 'Number' }, name => { is => 'String' }, value => { is => 'String'}, ], has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Number' }, ], has => [ params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1 }, # Actually, either of these property definitions will work interesting_param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1, where => [ name => 'interesting'] }, bob_param_value => { via => 'params', to => 'value', where => [name => 'bob'] }, #interesting_params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1, # where => [name => 'interesting']}, #interesting_param_values => { via => 'interesting_params', to => 'value', is_many => 1, is_mutable => 1 }, ], ); # make a non-interesting one ahead of time URT::Param->create(thing_id => 2, name => 'uninteresting', value => '123'); my $o = URT::Thing->create(thing_id => 2, interesting_param_values => ['abc','def']); ok($o, 'Created another Thing'); my @params = $o->params(); is(scalar(@params), 3, 'And it has 3 attached params'); isa_ok($params[0], 'URT::Param'); isa_ok($params[1], 'URT::Param'); isa_ok($params[2], 'URT::Param'); @params = sort { $a->value cmp $b->value } @params; is($params[0]->name, 'uninteresting', "param 1's name is uninteresting"); is($params[1]->name, 'interesting', "param 2's name is interesting"); is($params[2]->name, 'interesting', "param 3's name is interesting"); is($params[0]->value, '123', "param 1's value is correct"); is($params[1]->value, 'abc', "param 2's value is correct"); is($params[2]->value, 'def', "param 3's value is correct"); # Try to get the object again w/ id my $o2 = URT::Thing->get(2); ok($o2, 'Got thingy w/ id 2'); is_deeply([ $o->interesting_param_values ], [ $o2->interesting_param_values ], 'Ineresting values match those from orginal object'); my @o = URT::Thing->get(bob_param_value => undef); is(scalar(@o), 1, 'Got one thing back with no bob_param_value'); # Try to get the object again w/ id and ineresting values # FIXME does not work #my $o3 = URT::Thing->get( # thing_id => 2, # interesting_param_values => ['abc','def'], #); #ok($o3, 'Got thingy w/ id 2 and interesting_param_values => [qw/abc def/]'); #is_deeply([ $o->interesting_param_values ], [ $o3->interesting_param_values ], 'Ineresting values match those from original object'); 47b_indirect_is_many_accessor_mutable_with_id_class_by.t100664023532023421 2401012544604517 26175 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 84; UR::Object::Type->define( class_name => 'URT::Value1', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Value2', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Value3', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Param', id_by => [ thing_id => { is => 'Number' }, name => { is => 'String' }, value_class_name => { is => 'Text' }, value_id => { is => 'Text' }, ], has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'UR::Object', id_class_by => 'value_class_name', id_by => 'value_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Number' }, ], has => [ params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1 }, param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1 }, # Actually, either of these property definitions will work interesting_param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1, where => [ name => 'interesting'] }, #interesting_params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1, # where => [name => 'interesting']}, #interesting_param_values => { via => 'interesting_params', to => 'value', is_many => 1, is_mutable => 1 }, #< Test adding primitives, giving the class name friends => { via => 'params', to => 'value_id', is_many => 1, is_mutable => 1, where => [qw/ name friends value_class_name UR::Value /], }, ], ); my $v1 = URT::Value1->create(1); ok($v1, "made a test value 1"); my $v2 = URT::Value2->create(2); ok($v2, "made a test value 2"); my $v3 = URT::Value3->create(3); ok($v3, "made a test value 3"); ok("URT::Param"->can("value_id"), "created a property for value_id implicitly"); ok("URT::Param"->can("value_class_name"), "created a property for value_class_name implicitly"); #$DB::single = 1; #my $o1 = URT::Thing->create(thing_id => 2, param_values => [$v2,$v3]); my $o1 = URT::Thing->create(thing_id => 1); ok($o1, "created a test object which has-many of a test property"); #<># # test by direct construction of the bridge my $p = URT::Param->create(thing_id => 1, name => 'uninteresting', value => $v1); ok($p, "made an object with a value as a paramter"); is($p->value_class_name, ref($v1), "class name is set on the new object as expected"); is($p->value_id, $v1->id, "id is set on the new object as expected"); #$DB::single = 1; is($p->value,$v1,"got the value back"); my @p = $o1->params(); is(scalar(@p),1,"got a param"); is($p[0],$p, "got the expected param back"); my @pv = $o1->param_values(); is(scalar(@pv),1,"got a param value"); is($pv[0],$v1,"got expected value"); #<># note('test "add_param"'); my $p2 = $o1->add_param(name => 'interesting', value => $v2); ok($p2, "added param 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_param"'); #$DB::single = 1; ok($o1->remove_param($p2), "removed param 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_param_value"'); #$DB::single = 1; $p2 = $o1->add_param_value(name => 'interesting', value => $v2); ok($p2, "added another param"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_param_value"'); #$DB::single = 1; ok($o1->remove_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_interesting_param_value" with a key-value pair'); #$DB::single = 1; $p2 = $o1->add_interesting_param_value(value => $v2); ok($p2, "added an intereting param"); is($p2->name,'interesting', "the param name was set automatically during addition"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_interesting_param_value"'); #$DB::single = 1; ok($o1->remove_interesting_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_interesting_param_value" without a key-value pair'); #$DB::single = 1; $p2 = $o1->add_interesting_param_value($v2); ok($p2, "added an intereting param"); is($p2->name,'interesting', "the param name was set automatically during addition"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_interesting_param_value" again'); #$DB::single = 1; ok($o1->remove_interesting_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># #note("test setting an indirect value as a group"); #$o1->interesting_param_values(undef); #my @v = $o1->interesting_param_values; #ok(!@v, "no values associated after setting value to undef through has-many mutable accessor") # or diag(Data::Dumper::Dumper(\@v)); #@v = $o1->interesting_param_values([$v1,$v2,$v3]); #is("@v", "$v1 $v2 $v3", "correctly re-set the value list"); #<># #$DB::single = 1; my $thing2 = URT::Thing->create(thing_id => 2, interesting_param_values => [$v1,$v2,$v3]); ok($thing2, 'Created another Thing'); my @params = $thing2->params();; is(scalar(@params), 3, 'And it has 3 attached params'); isa_ok($params[0], 'URT::Param'); isa_ok($params[1], 'URT::Param'); isa_ok($params[2], 'URT::Param'); @params = sort { $a->value cmp $b->value } @params; is($params[0]->name, 'interesting', "param 1's name is interesting"); is($params[1]->name, 'interesting', "param 2's name is interesting"); is($params[2]->name, 'interesting', "param 3's name is interesting"); is($params[0]->value, $v1, "param 1's value is correct"); is($params[1]->value, $v2, "param 2's value is correct"); is($params[2]->value, $v3, "param 3's value is correct"); $v1->p1(1000); my @values = $thing2->param_values(p1 => 1000); is(scalar(@values), 1, "got one object back when filtering in an indirect accessor which is two steps away"); is($values[0], $v1, "got the correct object back when filtering in an indirect accessor which his two steps away"); @values = $thing2->param_values(); is(scalar(@values), 3, "got everything back when not filtering with an indirect accessor which is two steps away"); # Try to get the object again w/ id my $o2 = URT::Thing->get(2); ok($o2, 'Got thingy w/ id 2'); my @v = $o2->interesting_param_values; @v = sort { $a->id cmp $b->id } @v; my @expected = sort { $a->id cmp $b->id } ( $v1, $v2, $v3 ); is_deeply(\@v,\@expected, 'Ineresting values match those from orginal object'); #is_deeply([ $o1->interesting_param_values ], [ $thing2->interesting_param_values ], 'Ineresting values match those from orginal object'); #<># note('primitives with UR::Value in where clause'); $o1->add_friend('Watson'); is_deeply([$o1->friends], [qw/ Watson /], 'Added a friend: Watson'); $o1->add_friend('Crick'); is_deeply([sort $o1->friends], [qw/ Crick Watson /], 'Added a friend: Crick'); $o1->remove_friend('Watson'); is_deeply([$o1->friends], [qw/ Crick /], 'Removed a friend: Watson'); $o1->friends(undef); ok(!$o1->friends, 'Set friends to undef'); # Try to get the object again w/ id and ineresting values # FIXME does not work #my $o3 = URT::Thing->get( # thing_id => 2, # interesting_param_values => ['abc','def'], #); #ok($o3, 'Got thingy w/ id 2 and interesting_param_values => [qw/abc def/]'); #is_deeply([ $o->interesting_param_values ], [ $o3->interesting_param_values ], 'Ineresting values match those from original object'); 47c_is_many_accessor_with_id_class_by.t100664023532023421 434712544604517 22577 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 31; class URT::Note { id_by => [ id => { is => 'Number', len => 10 }, ], has => [ subject_class_name => { is => 'Text', len => 255 }, subject_id => { is => 'Text', len => 255 }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id' }, editor_id => { is => 'Text', len => 200 }, entry_date => { is => 'Date' }, header_text => { is => 'Text', len => 200 }, ], has_optional => [ body_text => { is => 'Text', len => 1000 }, ], }; class URT::Notable { is_abstract => 1, has => [ notes => { is => 'URT::Note', is_many => 1, reverse_as => 'subject', }, ], }; class URT::Foo { is => 'URT::Notable', }; class URT::Bar { is => 'URT::Foo' }; class URT::Baz { is => 'URT::Foo' }; ok(URT::Foo->isa("URT::Notable")); my $o1 = URT::Bar->create(100); ok($o1, "created a test notable object"); my $o2 = URT::Baz->create(200); ok($o2, "created another test notable object"); my @n; my $n; @n = $o1->notes; is(scalar(@n),0,"no notes at start"); @n = $o2->notes; is(scalar(@n),0,"no notes at start"); for my $o ($o1,$o2) { $n = $o->add_note( header_text => "head1", body_text => "body1", ); ok($n, "added a note"); is($n->header_text, 'head1', 'header is okay'); is($n->body_text, 'body1', 'body is okay'); #print Data::Dumper::Dumper($n); $n = $o->add_note( header_text => "head2", body_text => "body2", ); ok($n, "added a note"); is($n->header_text, 'head2', 'header is okay'); is($n->body_text, 'body2', 'body is okay'); #print Data::Dumper::Dumper($n); }; for my $o ($o1,$o2) { my @n = $o->notes; is(scalar(@n),2,"got two notes for the object"); for my $n (@n) { is($n->subject_class_name,ref($o),"class is set"); is($n->subject_id,$o->id,"id is set"); is($n->subject,$o,"object access works"); } } 1; 48_inline_datasources.t100664023532023421 2204612544604517 17415 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 43; use File::Temp; &setup_files_and_classes(); foreach my $class_name ( qw( URT::Office URT::Office2 URT::Employee URT::Employee2 URT::Employee3 URT::Employee4 )) { my $class_meta = UR::Object::Type->get($class_name); ok($class_meta, "Loaded class meta for $class_name"); my @ds_name_parts = $class_name =~ m/^(\w+)::(.*)/; my $expected_ds_name = join('::', shift(@ds_name_parts), 'DataSource', @ds_name_parts); is($class_meta->{'data_source_id'}, $expected_ds_name, "It has a data source named"); # my $ds_meta = UR::DataSource->get($class_meta->data_source); # ok($ds_meta, 'Loaded data source meta object'); }; # Try reading from the multi-file data source my $an_office = URT::Office2->get(office_id => 1); ok($an_office, 'Got office with id 1'); is($an_office->address, '123 Main St', 'Address is correct'); foreach my $emp_class ( qw( URT::Employee URT::Employee2 URT::Employee3 URT::Employee4 )) { my $employee = $emp_class->get(division => 'Europe', department => 'RnD', office_address => '345 Fake St'); ok($employee, "Loaded a $emp_class employee by address (delegated property)"); is($employee->emp_id, 5, 'emp_id is correct'); is($employee->name, 'John', 'name is correct'); is($employee->division, 'Europe', 'division is correct'); is($employee->department, 'RnD', 'department is correct'); } my $employee; $employee = eval { URT::Employee->get(); }; ok(!$employee, 'Correctly could not URT::Employee->get() with no params'); like($@, qr/Can't resolve data source: no division specified in rule/, "Error message mentions 'division' property"); my $error_message; UR::DataSource::FileMux->dump_error_messages(0); UR::DataSource::FileMux->error_messages_callback(sub { $DB::single=1; $error_message = $_[1]; }); $employee = eval { URT::Employee->get(division => 'NorthAmerica') }; ok(!$employee, 'Correctly could not URT::Employee->get() with only division'); like($@, qr/Can't resolve data source: no department specified in rule/, "Error message mentions 'department' property"); like($error_message, qr(Recursive entry.*URT::Employee), 'Error message did mention recursive call trapped'); my @employees = eval { URT::Employee->get(division => 'NorthAmerica', department => 'sales') }; ok(! scalar(@employees), 'URT::Employee->get() with non-existent department correctly returns no objects'); is($@, '', 'Correctly, no error message was generated'); @employees = eval { URT::Employee->get(division => 'NorthAmerica', department => 'finance') }; is(scalar(@employees), 3, 'Loaded 3 employees from NorthAmerica/finance'); eval { UR::Object::Type->define( class_name => 'URT::MissingColumnOrder', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { is => 'UR::DataSource::File', file_list => \@office_data_files, }, ); }; ok($@, "missing column_order throws an exception"); sub setup_files_and_classes { our $tmp_dir = File::Temp->newdir('inline_ds_XXXX', TMPDIR => 1, CLEANUP => 1); mkdir $tmp_dir; mkdir "${tmp_dir}/NorthAmerica"; mkdir "${tmp_dir}/Europe"; @office_data_files = ("${tmp_dir}/offices.csv", "${tmp_dir}/offices2.csv"); #our @files_to_remove_later = ( @office_data_files ); # Fill in the data foreach my $name ( @office_data_files ) { my $f = IO::File->new(">$name"); $f->print("1, 123 Main St\n"); $f->print("4, 345 Fake St\n"); $f->print("5, 1 Office Complex Ct\n"); $f->print("100, One Hundred\n"); $f->print("123, 123 Main St\n"); $f->print("350, The Penthouse\n"); $f->close(); } # Yer basic datasource UR::Object::Type->define( class_name => 'URT::Office', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { # This one fills in all the required info is => 'UR::DataSource::File', file => $office_data_files[0], column_order => ['office_id', 'address'], sort_order => ['office_id'], skip_first_line => 0, }, ); # This one discovers columns and sort columns from the class data, and # can read from a list of files UR::Object::Type->define( class_name => 'URT::Office2', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { is => 'UR::DataSource::File', column_order => ['office_id', 'address'], file_list => \@office_data_files, }, ); unshift @files_to_remove_later, &employee_file_resolver('NorthAmerica','finance'); $f = IO::File->new(">$files_to_remove_later[0]"); $f->print("1\tBob\t100\n"); $f->print("2\tFred\t123\n"); $f->print("3\tJoe\t350\n"); $f->close(); unshift @files_to_remove_later, &employee_file_resolver('Europe', 'RnD'); $f = IO::File->new(">$files_to_remove_later[0]"); $f->print("1\tMike\t1\n"); $f->print("5\tJohn\t4\n"); $f->print("6\tRick\t5\n"); $f->close(); # This one pivots between the two files create above with a function UR::Object::Type->define( class_name => 'URT::Employee', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ 'emp_id', 'name', 'office_id' ], sort_order => [ 'emp_id' ], constant_values => ['division','department'], required_for_get => ['division', 'department'], resolve_path_with => \&employee_file_resolver, }, ); # This one is the same as above, but uses alternate syntax with 'resolve_path_with' UR::Object::Type->define( class_name => 'URT::Employee2', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], resolve_path_with => [\&employee_file_resolver, 'division', 'department'], }, ); # This one uses resolve_path_with with a base_path and list of properties UR::Object::Type->define( class_name => 'URT::Employee3', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], base_path => $tmp_dir, resolve_path_with => ['division','department'], }, ); # This one uses resolve_path_with with an sprintf format UR::Object::Type->define( class_name => 'URT::Employee4', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], resolve_path_with => ["${tmp_dir}/%s/%s", 'division','department'], }, ); } sub employee_file_resolver { my($division, $department) = @_; our $tmp_dir; sprintf("${tmp_dir}/$division/$department"); } 49_complicated_get.t100664023532023421 1256412544604517 16672 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The subclass we're get()ting has no table of its own; it inherits one from its parent # - The property we're get()ting with isn't a column in its inherited table, it's delegated # - That delegated property is 'via' another subclass with no table of its own # - The delegated property is 'to' another delegated property # # UR::DataSource::RDBMS was modified to properly determine table/column when the subclass # inherits that table/column from a parent. It also needed to traverse delegated properties # to arbitrary depth to know what the final accessor is. &setup_classes_and_db(); my $thing = URT::Thing::Person->get(job => 'cook'); ok($thing, 'get() returned an object'); isa_ok($thing, 'URT::Thing::Person'); is($thing->name, 'Bob', 'The expected object was returned'); is($thing->job, 'cook', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing_type (type_id integer, type_name varchar)"), 'Created type table'); ok( $dbh->do("create table thing (thing_id integer, name varchar, type_id integer REFERENCES thing_type(type_id))"), 'Created thing table'); ok( $dbh->do("create table param (param_id integer, type varchar, value varchar, type_id integer REFERENCES thing_type(type_id))"), 'Created param table'); my $ins_type = $dbh->prepare("insert into thing_type (type_id, type_name) values (?,?)"); foreach my $row ( ( [1, 'person'], [2, 'car'] ) ) { ok( $ins_type->execute(@$row), 'Inserted a type'); } my $ins_thing = $dbh->prepare("insert into thing (thing_id, name, type_id) values (?,?,?)"); foreach my $row ( ( [1, 'Bob',1], [2, 'Christine',2]) ) { ok( $ins_thing->execute(@$row), 'Inserted a thing'); } $ins_thing->finish; my $ins_params = $dbh->prepare("insert into param (param_id, type, value, type_id) values (?,?,?,?)"); foreach my $row ( ( [1, 'alignment', 'good', 1], [2, 'job', 'cook', 1], [3, 'alignment', 'evil', 2], [4, 'color', 'red', 2] ) ) { ok($ins_params->execute(@$row), 'Inserted a param'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::ThingType', id_by => [ type_id => { is => 'Integer' }, ], has => [ type_name => { is => 'String' }, params => { is => 'URT::Param', reverse_as => 'type_obj', is_many => 1 }, alignment => { via => 'params', to => 'value', where => [param_type => 'alignment'] }, ], is_abstract => 1, sub_classification_method_name => '_type_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing_type', ); UR::Object::Type->define( class_name => 'URT::ThingType::Person', is => 'URT::ThingType', has => [ job => { via => 'params', to => 'value', where => [type => 'job'] }, ] ); UR::Object::Type->define( class_name => 'URT::ThingType::Car', is => 'URT::ThingType', has => [ color => { via => 'params', to => 'value', where => [type => 'color'] }, ] ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, type_obj => { is => 'URT::ThingType', id_by => 'type_id' }, type => { via => 'type_obj', to => 'type_name' }, params => { via => 'type_obj' }, alignment => { via => 'params' }, ], is_abstract => 1, #sub_classification_property_name => 'type', sub_classification_method_name => '_thing_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Thing::Person', is => 'URT::Thing', has => [ type_obj => { is => 'URT::ThingType::Person', id_by => 'type_id' }, job => { via => 'type_obj' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing::Car', is => 'URT::Thing', has => [ type_obj => { is => 'URT::ThingType::Car', id_by => 'type_id' }, color => { via => 'type_obj' }, ], ); UR::Object::Type->define( class_name => 'URT::Param', id_by => 'param_id', has => [ type => { is => 'String' }, value => { is => 'String' }, type_obj => { is => 'URT::ThingType', id_by => 'type_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'param', ); } sub URT::Thing::_thing_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type); } sub URT::ThingType::_type_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type_name); } 49b_complicated_get_2.t100664023532023421 1064212544604517 17250 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - the final property/column we're filtering on is on the parent class of the delegation &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_name => 'Collie'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id))"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id))"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name) values (?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart) values (?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id) values (?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id) values (?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie'); $breed_insert->execute(1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie'); $animal_insert->execute(2, 1); # a person named Jeff $name_insert->execute(3, 'Jeff'); $person_insert->execute(3,2); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => 'named_thing', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'breed', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); } 49c_complicated_get_3.t100664023532023421 1051312544604517 17247 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_is_smart => 1); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id))"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id))"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name) values (?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart) values (?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id) values (?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id) values (?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie'); $breed_insert->execute(1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie'); $animal_insert->execute(2, 1); # a person named Jeff $name_insert->execute(3, 'Jeff'); $person_insert->execute(3,2); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => 'named_thing', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'breed', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); } 49d_complicated_get_joining_through_view.t100664023532023421 1134212544604517 23336 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - All the "tables" involved are areally inline views &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_is_smart => 1); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL, do_include integer)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL, do_include integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id), do_include integer)"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id), do_include integer)"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name, do_include) values (?,?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart, do_include) values (?,?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id, do_include) values (?,?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id, do_include) values (?,?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie',1); $breed_insert->execute(1,1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie',1); $animal_insert->execute(2, 1,1); # a person named Jeff $name_insert->execute(3, 'Jeff',1); $person_insert->execute(3,2,1); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from named_thing where do_include = 1) named_thing_view', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from breed where do_include = 1) breed_view', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from animal where do_include = 1) animal_view', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from person where do_include = 1) person_view', ); } 49e_complicated_get_joining_through_view2.t100664023532023421 1150212544604517 23417 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - the final property/column we're filtering on is on the parent class of the delegation # - All the "tables" involved are really inline views &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_name => 'Collie'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL, do_include integer)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL, do_include integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id), do_include integer)"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id), do_include integer)"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name, do_include) values (?,?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart, do_include) values (?,?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id, do_include) values (?,?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id, do_include) values (?,?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie',1); $breed_insert->execute(1,1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie',1); $animal_insert->execute(2, 1,1); # a person named Jeff $name_insert->execute(3, 'Jeff',1); $person_insert->execute(3,2,1); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from named_thing where do_include = 1) named_thing_view', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from breed where do_include = 1) breed_view', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from animal where do_include = 1) animal_view', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from person where do_include = 1) person_view', ); } 49f_complicated_get_indirect_id_by.t100664023532023421 1117512544604517 22064 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # This tests a get() where the filtering property has several levels of indirection # - A Person has a Job, which has a Location, which has a phone number # - A Person's phone number for their job can be linked together a couple # of different ways # 1) # a) Location has a phone number # b) Job has-a location, id-by the location_id property # c) Job has-a phone number via the Location # d) Person has-a Job, linked with the job_id property # e) Person has-a job_phone, via the Job (which is via the Location) # 2) a,b same as above # c) Person has-a location_id via the Job # d) Person has-a Location, id-by the location_id (which is via the Job) # e) Person has-a work_phone, via the Location &setup_classes_and_db(); # This is the way we usually do a doubly-indirect property my $person = URT::Person->get(job_phone => '456-789-0123'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Joe', 'Got the right person'); is($person->job_name, 'cleaner', 'With the right job name'); is ($person->job_phone, '456-789-0123', 'the right job_phone'); is ($person->work_phone, '456-789-0123', 'and the right work_phone'); # This one wasn't working before I fixed UR::Object::Property::_get_joins() $person = URT::Person->get(work_phone => '123-456-7890'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Bob', 'Got the right person'); is($person->job_name, 'cook', 'With the right job name'); is($person->job_phone, '123-456-7890', 'the right job_phone'); is($person->work_phone, '123-456-7890', 'and the right work_phone'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table locations (location_id integer, phone_number varchar, address varchar)"), 'Created locations table'); ok( $dbh->do("create table jobs (job_id integer, job_name varchar, location_id integer REFERENCES locations(location_id))"), 'Created jobs table'); ok( $dbh->do("create table persons (person_id integer, name varchar, job_id integer REFERENCES jobs(job_id))"), 'Created persons table'); # First person $dbh->do("insert into locations (location_id, phone_number, address) values (1,'123-456-7890','123 Fake St')"); $dbh->do("insert into jobs (job_id, job_name, location_id) values (1, 'cook', 1)"); $dbh->do("insert into persons (person_id, name, job_id) values(1,'Bob', 1)"); # second $dbh->do("insert into locations (location_id, phone_number, address) values (2,'456-789-0123','987 Main St')"); $dbh->do("insert into jobs (job_id, job_name, location_id) values (2, 'cleaner', 2)"); $dbh->do("insert into persons (person_id, name, job_id) values(2,'Joe', 2)"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Location', id_by => [ location_id => { is => 'Integer' }, ], has => [ phone_number => { is => 'String' }, address => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'locations', ); UR::Object::Type->define( class_name => 'URT::Job', id_by => [ job_id => { is => 'Integer' }, ], has => [ job_name => { is => 'String' }, location => { is => 'URT::Location', id_by => 'location_id' }, location_phone => { via => 'location', to => 'phone_number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'jobs', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, job_id => { is => 'Integer' }, job => { is => 'URT::Job', id_by => 'job_id' }, job_name => { via => 'job' }, job_phone => { via => 'job', to => 'location_phone' }, work_location_id => { via => 'job', to => 'location_id' }, work_location => { is => 'URT::Location', id_by => 'work_location_id' }, work_phone => { via => 'work_location', to => 'phone_number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'persons', ); } 49g_complicated_get_double_join.t100664023532023421 414512544604517 21366 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 1; # This tests a get() where the same tabe/column (attribute.value) is getting filtered with # diggerent values as a result of two different properties (name and sibling_name) # # The SQL writer was getting confused by the time it got to the WHERE clause, and # applied them both the whatever alias was used for the final join to that table my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL, sibling_id integer)'); $dbh->do('create table attribute (person_id integer REFERENCES person(person_id), key varchar NOT NULL, value varchar, PRIMARY KEY (person_id, key))'); # Make 2 people named Bob and Fred, they are siblings $dbh->do("insert into person values (1, 2)"); $dbh->do("insert into attribute values (1,'name','Bob')"); $dbh->do("insert into person values (2, 1)"); $dbh->do("insert into attribute values (2,'name','Fred')"); UR::Object::Type->define( class_name => 'Person', table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has => [ attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, name => { is => 'String', via => 'attributes', where => [key => 'name'], to => 'value' }, sibling => { is => 'Person', id_by => 'sibling_id' }, sibling_name => { via => 'sibling', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Attribute', table_name => 'attribute', data_source => 'URT::DataSource::SomeSQLite', id_by => [ person_id => { is => 'Integer' }, key => { is => 'String' }, ], has => [ person => { is => 'Person', id_by => 'person_id'}, value => { is => 'String' }, ], ); my @p = Person->get(name => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person'); 49h_complicated_get_double_join.t100664023532023421 625312544604517 21371 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 2; # Similar to the other double-join test. The same table gets joined in and needs a different filter # for each join. # # This test is different in that there is an additional join between the two person objects, and # the property names end up sorting in different orders between test 1 and 2 my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL)'); $dbh->do('create table attribute (attr_id integer PRIMARY KEY NOT NULL, person_id integer NOT NULL REFERENCES person(person_id), name varchar NOT NULL, value varchar)'); $dbh->do('create table relationship (person_id integer REFERENCES person(person_id), related_person_id integer REFERENCES person(person_id), name varchar NOT NULL, PRIMARY KEY (person_id, related_person_id))'); # Make 2 people named Bob and Fred, they are siblings $dbh->do("insert into person values (1)"); $dbh->do("insert into attribute values (1,1,'name', 'Bob')"); $dbh->do("insert into person values (2)"); $dbh->do("insert into attribute values (3,2,'name', 'Fred')"); $dbh->do("insert into relationship values (1,2,'sibling')"); $dbh->do("insert into relationship values (2,1,'sibling')"); UR::Object::Type->define( class_name => 'Person', table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has_many => [ attributes => { is => 'Attribute', reverse_as => 'person' }, relationships => { is => 'Relationship', reverse_as => 'person' }, ], has => [ name => { via => 'attributes', to => 'value', where => [name => 'name']}, zname => { via => 'attributes', to => 'value', where => [name => 'name']}, sibling => { is => 'Person', via => 'relationships', to => 'related_person', where => [name => 'sibling'] }, sibling_name => { via => 'sibling', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Attribute', table_name => 'attribute', data_source => 'URT::DataSource::SomeSQLite', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, name => { is => 'String' }, value => { is => 'String' }, ], ); UR::Object::Type->define( class_name => 'Relationship', table_name => 'relationship', data_source => 'URT::DataSource::SomeSQLite', id_by => [ person_id => { is => 'Integer' }, related_person_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id'}, related_person => { is => 'Person', id_by => 'related_person_id' }, name => { is => 'String' }, ], ); my @p = Person->get(name => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person joining name before sibling'); @p = Person->get(zname => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person joining name after sibling'); 49i_complicated_get_join_through_value_class.t100664023532023421 517512544604517 24163 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 3; # tests a get() where the delegated property's join chain has a UR::Value class in the middle # # Before the fix, the QueryPlan would see that UR::Values are not resolvable in the DB, and so # stops trying to connect joins together, leading to multiple queries. The fix was to splice # out these non-db joins while constructing the SQL my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, person_id integer references person(person_id), key varchar, value varchar)'); $dbh->do('create table car (car_id integer not null primary key, make varchar, model varchar)'); $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into car values (2,'Chevrolet','Impala')"); $dbh->do("insert into attribute values (3,1,'car_id', 2)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, car_id => { is => 'Integer', via => 'attributes', to => 'value', where => [key => 'car_id'] }, car => { is => 'Car', id_by => 'car_id' }, car_make => { is => 'String', via => 'car', to => 'make' }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); UR::Object::Type->define( class_name => 'Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ make => { is => 'String' }, model => { is => 'String' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my $p = Person->get(car_make => 'Chevrolet'); ok($p, 'Got the person'); is($query_count, 1, 'Made one query'); 49j_complicated_get_join_ends_at_value_class.t100664023532023421 611112544604517 24110 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 9; # tests a get() where the a UR::Value-related property is in the hints list, but # in order to satisfy that hint, it needs to join to the attribute table to retrieve # the Value's ID # # Before the fix, the QueryPlan had a couple of issues # 1) The delegated properties loop would remove all joins through UR::Value classes, # leaving the @join list empty. At the snd of the delegation loop, # $last_class_object_excluding_inherited_joins had not been set, and so it dies # 2) UR::Object::Join::resolve_forward() would not recursivly find and joins required # to fulfill the id_by property. my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table disk (disk_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, disk_id integer references disk(disk_id), key varchar, value varchar)'); $dbh->do("insert into disk values (1,'boot')"); $dbh->do("insert into attribute values (3,1,'size_bytes', 2097152)"); # 2048K UR::Object::Type->define( class_name => 'Disk::Value::KBytes', is => 'UR::Value', ); sub Disk::Value::KBytes::__display_name__ { my $size = shift->id; my $kbytes = $size / 1024; return $kbytes."K"; } UR::Object::Type->define( class_name => 'Disk', data_source => 'URT::DataSource::SomeSQLite', table_name => 'disk', id_by => [ disk_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'disk', is_many => 1 }, size => { via => 'attributes', to => 'value', where => [key => 'size_bytes'] }, pretty_size_kbytes => { is => 'Disk::Value::KBytes', id_by => 'size' }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ disk => { is => 'Disk', id_by => 'disk_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @d = Disk->get(-hints => ['pretty_size_kbytes']); is(scalar(@d), 1, 'Got the object'); is($query_count, 1, 'Made one query'); $query_count = 0; my $value_obj = $d[0]->pretty_size_kbytes; ok($value_obj, 'Got the value object for size'); is($query_count, 0, 'Made no queries'); $query_count = 0; is($value_obj->id, $d[0]->size, 'The ID of the value object matches the original object size'); is($query_count, 0, 'Made no queries'); $query_count = 0; is($value_obj->__display_name__, "2048K", '__display_name__ for Value object is correct'); is($query_count, 0, 'Made no queries'); 49k_complicated_get_joins_with_hangoff_filter.t100664023532023421 600612544604517 24311 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 6; # This test does a query that joins two different tables twice each into a single query # (for a total of 4 tables joined) for a different "reason" each time. # # Loading a row shouldn't cause any additional queries my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, person_id integer not null, key varchar, value varchar)'); # Bob and Fred are people $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into person values (2,'Fred')"); # Bob lives at 123 main st and has a green car $dbh->do("insert into attribute values (11,1,'address','123 main st')"); $dbh->do("insert into attribute values (12,1,'car_color','green')"); # Fred lives at 456 elm st and has a red car $dbh->do("insert into attribute values (21,2,'address','456 elm st')"); $dbh->do("insert into attribute values (22,2,'car_color','red')"); # Bob's father is Fred $dbh->do("insert into attribute values (13,1,'father_id', 2)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, address => { is => 'String', via => 'attributes', to => 'value', where => [key => 'address'] }, father_id => { is => 'Integer', via => 'attributes', to => 'value', where => [key => 'father_id'], is_optional => 1 }, father => { is => 'Person', id_by => 'father_id', is_optional => 1 }, father_address => { via => 'father', to => 'address', is_optional => 1 }, car_color => { is => 'String', via => 'attributes', to => 'value', where => [ key => 'car_color' ] }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my $iter = Person->create_iterator(father_address => '456 elm st'); ok($iter, 'Created iterator for people filter by father_address'); is($query_count, 1, 'Made one query'); $query_count = 0; my $p = $iter->next(); ok($p, 'Got a person'); is($p->name, 'Bob', 'It was the right person'); is($query_count, 0, 'Made no queries'); 49l_complicated_get_id_by_attribute.t100664023532023421 753012544604517 22254 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 7; # This test does a query involving an object accessor, where its id_by is indirect # through a filtered attribute table. We're making sure the join from person to # car_attribute includes conditions on both person.person_id = attr.value _and_ # attr.key = 'owner_id'. # # Without that second condition, the query for green cars joins to both people # because the green car has owner_id 1 and driver_id 2 my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table car (car_id integer not null primary key, color varchar not null)'); $dbh->do('create table car_attribute (attr_id integer not null primary key, car_id integer not null, key varchar, value varchar)'); # Bob and Fred are people $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into person values (2,'Fred')"); # Bob has a green and yellow car, Fred has a black car $dbh->do("insert into car values (1, 'green')"); $dbh->do("insert into car values (2, 'yellow')"); $dbh->do("insert into car values (3, 'black')"); $dbh->do("insert into car_attribute values (1, 1, 'owner_id', 1)"); $dbh->do("insert into car_attribute values (2, 1, 'driver_id', 2)"); # Also, Fred drives Bob's green car $dbh->do("insert into car_attribute values (4, 1, 'owner_id', 1)"); $dbh->do("insert into car_attribute values (7, 3, 'owner_id', 2)"); UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Text' }, ], has => [ name => { is => 'String' }, ], has_many => [ cars => { is => 'URT::Car', reverse_as => 'owner' }, car_colors => { via => 'cars', to => 'color' }, ] ); UR::Object::Type->define( class_name => 'URT::Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ color => { is => 'Text' }, owner_id => { is => 'Text', via => 'attributes', to => 'value', where => ['key' => 'owner_id']}, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], has_many => [ attributes => { is => 'URT::CarAttribute', reverse_as => 'car' }, ], ); UR::Object::Type->define( class_name => 'URT::CarAttribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car_attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ car => { is => 'URT::Car', id_by => 'car_id' }, key => { is => 'Text', }, value => { is => 'Text', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(car_colors => 'green'); is(scalar(@p), 1, 'Got one person with a green car'); is($query_count, 1, 'Made 1 query'); $query_count = 0; is($p[0]->name, 'Bob', 'It is the right person'); is($query_count, 0, 'Made 0 queries'); # If the query by car_colors worked properly, then this get() should not hit the DB # because it was loaded as part of the join connecting the car with its owner $query_count = 0; my $a = URT::CarAttribute->get(1); is($query_count, 0, 'Getting car attribute ID 1 took no DB queries'); # But this should hit the DB, because it was for the 'driver_id' attribute, not owner_id $query_count = 0; $a = URT::CarAttribute->get(2); is($query_count, 1, 'Getting car attribute ID 2 (driver_id) took 1 DB query'); 49m_reverse_as_is_delegated.t100664023532023421 1115412544604517 20545 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 19; # This test does a query that joins three tables. # The get() is done on an is-many property, and its reverse_as is a delegated # property through a third class my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table car (car_id integer not null primary key, model varchar not null)'); $dbh->do('create table driver (driver_id integer not null primary key, name varchar not null)'); $dbh->do('create table car_driver (car_id integer not null references car(car_id), driver_id integer not null references driver(driver_id))'); $dbh->do("insert into car values (1,'batmobile')"); $dbh->do("insert into car values (2,'race car')"); $dbh->do("insert into car values (3,'mach 5')"); $dbh->do("insert into car values (4,'junked car')"); $dbh->do("insert into driver values (1,'batman')"); $dbh->do("insert into driver values (2,'mario')"); $dbh->do("insert into driver values (3,'speed racer')"); $dbh->do("insert into driver values (4,'superman')"); # batman drives the batmobile $dbh->do("insert into car_driver values (1,1)"); # mario and speed racer drive the race car $dbh->do("insert into car_driver values (2,2)"); $dbh->do("insert into car_driver values (2,3)"); # speed racer also drives the mach 5 $dbh->do("insert into car_driver values (3,3)"); # superman doesn't drive anything # no one drives the junked car UR::Object::Type->define( class_name => 'URT::Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ model => { is => 'String', }, ], has_many => [ car_drivers => { is => 'URT::CarDriver', reverse_as => 'car' }, drivers => { is => 'URT::Driver', via => 'car_drivers', to => 'driver' }, # regular many-to-many property def'n driver_names => { is => 'String', via => 'drivers', to => 'name' }, ], ); UR::Object::Type->define( class_name => 'URT::Driver', data_source => 'URT::DataSource::SomeSQLite', table_name => 'driver', id_by => [ driver_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], has_many => [ cars => { is => 'URT::Car', reverse_as => 'drivers' }, # not the usual way to make a many-to-many property def'n car_models => { is => 'String', via => 'cars', to => 'model' }, ], ); UR::Object::Type->define( class_name => 'URT::CarDriver', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car_driver', id_by => [ car_id => { is => 'Integer' }, driver_id => { is => 'Integer' }, ], has => [ car => { is => 'URT::Car', id_by => 'car_id' }, driver => { is => 'URT::Driver', id_by => 'driver_id' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); $query_count = 0; my $driver = URT::Driver->get(name => 'batman'); ok($driver, 'got the batman driver'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @cars = $driver->cars(); is(scalar(@cars), 1, 'batman drives 1 car'); is($query_count, 1, 'Made 1 query'); is($cars[0]->model, 'batmobile', 'It is the right car'); $query_count = 0; @cars = $driver->cars(); is(scalar(@cars), 1, 'trying again, batman drives 1 car'); TODO: { local $TODO = "query cache doesn't track properties like drivers.id"; is($query_count, 0, 'Made no queries'); } is($cars[0]->model, 'batmobile', 'It is the right car'); $query_count = 0; my @models = $driver->car_models(); is(scalar(@models), 1, 'batman has 1 car model'); is_deeply(\@models, ['batmobile'], 'Got the right car'); is($query_count, 0, 'Made 0 queries'); $driver = URT::Driver->get(name => 'speed racer'); ok($driver, 'Got speed racer'); $query_count = 0; @models = $driver->car_models(); is(scalar(@models), 2, 'speed racer drives 2 cars'); @models = sort @models; is_deeply(\@models, ['mach 5', 'race car'], 'Got the right cars'); is($query_count, 1, 'Made 1 query'); $driver = URT::Driver->get(name => 'superman'); ok($driver, 'Got superman'); $query_count = 0; @models = $driver->car_models(); is(scalar(@models), 0, 'superman drives 0 cars'); TODO: { local $TODO = "UR::BX::Template->resolve needs to support meta opt -hints to make this work"; is($query_count, 1, 'Made 1 query'); } 49n_double_join_involves_inheritance.t100664023532023421 1062212544604517 22505 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # the initial code is from test 91b, to set-up some joinable data use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar)'), 'created person table'); ok($dbh->do('create table VEHICLE ( vehicle_id int NOT NULL PRIMARY KEY, color varchar, subclass_name varchar)'), 'created car table'); ok($dbh->do('create table REGISTRATION (registration_id int NOT NULL PRIMARY KEY, vehicle_id integer, vehicle_class_name varchar, owner_id integer references PERSON(person_id), type varchar)'), 'created registration table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, registrations => { is => 'URT::Registration', reverse_as => 'owner', is_many => 1 }, vehicles => { is => 'URT::Vehicle', via => 'registrations', to => 'vehicle' }, primary_car => { is => 'URT::Car', via => 'registrations', to => 'vehicle', where => [type => 'primary'], is_optional => 1 }, secondary_car => { is => 'URT::Car', via => 'registrations', to => 'vehicle', where => [type => 'secondary'], is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Vehicle', is_abstract => 1, table_name => 'VEHICLE', subclassify_by => 'subclass_name', id_by => [ vehicle_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, registrations => { is => 'URT::Registration', reverse_as => 'vehicle', is_many => 1 }, owner => { is => 'URT::Person', via => 'registrations', to => 'owner' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for Vehicle'); ok(UR::Object::Type->define( class_name => 'URT::Car', is => ['URT::Vehicle'], ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Registration', table_name => 'REGISTRATION', id_by => [ registration_id => { is => 'Number' }, ], has => [ owner => { is => 'URT::Person', id_by => 'owner_id' }, vehicle_id => { is => 'Number' }, vehicle_class_name => { is => 'Text' }, vehicle => { is => 'URT::Vehicle', id_by => 'vehicle_id', id_class_by => 'vehicle_class_name' }, type => { is => 'Number' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?)'); foreach my $row ( [ 11, 'Bob' ], [12, 'Fred'] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into vehicle values (?,?,?)'); foreach my $row ( [ 1,'red','URT::Car'], [ 2,'blue','URT::Car'], [3,'red','URT::Car'],[4,'yellow','URT::Car']) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into registration values (?,?,?,?,?)'); foreach my $row ( [101,1,'URT::Car',11,'primary'], [102,2,'URT::Car',11,'secondary'], [103,3,'URT::Car',12,'primary'], [104,4,'URT::Car',12,'secondary']) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; # chain property equiv my $bx1 = URT::Person->define_boolexpr('primary_car.vehicle_id' => 1, 'secondary_car.vehicle_id' => 2); ok($bx1, "got bx with property chain"); my @p1 = URT::Person->get($bx1); is(scalar(@p1), 1, "got one person with the requested cars using a property chain"); my @p2 = URT::Person->get('primary_car.color' => 'red', 'secondary_car.color' => 'yellow'); is(scalar(@p2), 1, "got one person with cars by color"); isnt($p1[0], $p2[0], 'the person with a yellow car is not the person with vehicle 1'); 50_force_always_reload.t100664023532023421 717712544604517 17527 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More skip_all => 'in development'; #tests => 34; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; &setup_classes_and_db($dbh); is(UR::Context->current->query_underlying_context, undef, 'Initial value for query_underlying_context is undef'); my $query_count = 0; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); UR::Context->current->query_underlying_context(1); $query_count = 0; my $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1'); is($query_count,1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1 again'); is($query_count,1, 'Made 1 query again'); $query_count = 0; my @things = URT::Thing->get('id <' => 100); is(scalar(@things), 3, 'Got all 3 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get('id <' => 100); is(scalar(@things), 3, 'Got all 3 things again'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1 again'); is($query_count,1, 'Made 1 query again'); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count,1, 'Made 1 query again'); $query_count = 0; $thing = URT::Thing->get(4); ok(! $thing, 'No thing with ID 4'); is($query_count,1, 'Made 1 query again'); UR::Context->current->query_underlying_context(undef); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count, 0, 'Made no queries because query_underlying_context is undef'); $query_count = 0; $thing = URT::Thing->get(4); ok(! $thing, 'No thing with ID 4'); is($query_count, 0, 'Made no queries because query_underlying_context is undef and query was done before'); ok($dbh->do("insert into thing values (10, 'Bubba', 'Person', 'red')"), 'insert new row into table'); $query_count = 0; @things = URT::Thing->get('id <' => 100); is(scalar(@things), 4, 'There are now 4 things'); is($query_count, 1, 'Made 1 query, even though get() was done before'); UR::Context->current->query_underlying_context(0); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count, 0, 'Made no queries, query_underlying_context is 0'); $query_count = 0; $thing = URT::Thing->get(5); ok(! $thing, 'No thing with ID 5'); is($query_count, 0, 'Made no queries because query_underlying_context is 0'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 4, 'Got all 4 things'); is($query_count, 0, 'Made no queries because query_underlying_context is 0'); sub setup_classes_and_db { my $dbh = shift; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created thing table'); my $ins_things = $dbh->prepare("insert into thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a thing'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'color', 'type' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 50_get_and_reload.t100664023532023421 1340612544604517 16462 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 64; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; &setup_classes_and_db($dbh); foreach my $class ( 'URT::Thing', 'URT::SubclassedThing' ) { # try load() as an object method my $thing = $class->get(1); ok($thing, 'get() returned an object'); isa_ok($thing, $class); is($thing->name, 'Bob', 'name is correct'); is($thing->color, 'green', 'color is correct'); my $table_name = $class->__meta__->table_name; my $sth = $dbh->prepare("update $table_name set color = 'purple' where thing_id = 1"); ok($sth->execute(), 'updated the color'); $sth->finish; $dbh->commit; is($thing->color, 'green', 'Before load() it still has the old color'); my $cx = UR::Context->current; ok($cx->reload($thing), 'Called load()'); is($thing->color, 'purple', 'After load() it has the new color'); # try load() as a class method() my @things = $class->get(name => 'Fred'); is(scalar(@things),1, 'Got one thing named Fred'); is($things[0]->color, 'black', 'color is correct'); $sth = $dbh->prepare("update $table_name set color = 'yellow' where name = 'Fred'"); ok($sth->execute(), 'updated the color'); $sth->finish; $dbh->commit; @things = $cx->reload($class, name => 'Fred'); is(scalar(@things),1, 'Again, got one thing named Fred'); is($things[0]->color, 'yellow', 'new color is correct'); # try updating both the object and DB, and see if it'll reload @things = $class->get(3); is(scalar(@things),1, 'Got one thing with id 3'); is($things[0]->color, 'red', 'its color is red'); $sth = $dbh->prepare("update $table_name set color = 'orange' where thing_id = 3"); ok($sth->execute(), 'updated the color in the DB'); $sth->finish; $dbh->commit; ok($things[0]->color('blue'), 'updated the color on the object'); my $worked = eval { $cx->reload($things[0]) }; ok(! $worked, 'calling load() on the changed object correctly fails'); my $message = $@; $message =~ s/\s+/ /gm; like($message, qr/A change has occurred in the database for $class property 'color' on object ID 3 from 'red' to 'orange'. At the same time, this application has made a change to that value to 'blue'./, 'Error message looks correct'); is($things[0]->color, 'blue', 'color remains what we set it to'); #is($things[0]->{'db_committed'}->{'color'}, 'orange', 'db_committed for the color was updated to what we set the database to'); is(UR::Context->_get_committed_property_value($things[0],'color'), 'orange', 'db_committed for the color was updated to what we set the database to'); # We now have to make that last object look like it's unchanged or the next get() will # also throw an exception $things[0]->color($things[0]->{'db_committed'}->{'color'}); @things = $class->get(); is(scalar(@things), 3, 'get() with no filters returns all the things'); $sth = $dbh->prepare("update $table_name set color = 'white'"); ok($sth->execute(), 'updated the color for all things'); $sth->finish; $dbh->commit; $thing = $cx->reload($class, 1); is($thing->color, 'white', 'load() for thing_id 1 has the changed color'); @things = $cx->reload($class); foreach my $thing ( @things ) { is($thing->color, 'white', 'load() for all things has the changed color for this object'); } } sub setup_classes_and_db { my $dbh = shift; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created thing table'); my $ins_things = $dbh->prepare("insert into thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a thing'); } ok( $dbh->do("create table subclassed_thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created subclassed_thing table'); $ins_things = $dbh->prepare("insert into subclassed_thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a subclassed_thing'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'color', 'type' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing', id_by => 'thing_id', has => ['name', 'color', 'type' ], is_abstract => 1, sub_classification_method_name => '_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'subclassed_thing', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing::Person', is => 'URT::SubclassedThing', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing::Car', is => 'URT::SubclassedThing', data_source => 'URT::DataSource::SomeSQLite', ); } sub URT::SubclassedThing::_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type); } 50_load_objects_that_stringify_false.t100664023532023421 213212544604517 22425 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 6; # This tests some fixes to the Context loading code that was being sloppy with # objects with stringify overloading. A simple boolean test on an object like # if ($object) {... # would stringify the object and then test that string for truthness. If the # string was "" or "0", then it would be boolean false package URT::Thing; use overload ( '""' => \&stringify ); UR::Object::Type->define( class_name => 'URT::Thing', is => 'UR::Value', ); sub stringify { return "" }; # always stringify to false package main; my $o = URT::Thing->get(1); ok(defined($o), 'Got Thing with id 1'); is($o->id, 1, 'It has the right ID'); $o = URT::Thing->get(0); ok(defined($o), 'Got Thing with id 0'); is($o->id, 0, 'It has the right ID'); my @o = URT::Thing->get([4,7,10,99,1]); is(scalar(@o), 5, 'Got 5 Things by ID'); is_deeply([map { $_->id} @o], [1,10,4,7,99], 'All the IDs were correct'); 50_unload_and_reload.t100664023532023421 2015512544604517 17164 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 86; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table thing (thing_id integer PRIMARY KEY, color varchar)'); $dbh->do("insert into thing values (1,'blue')"); $dbh->do("insert into thing values (2,'red')"); $dbh->do("insert into thing values (3,'green')"); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ color => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); my $query_count; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); # First, try with a single object $query_count = 0; my $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; my $cx = UR::Context->current; $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 with reload'); is($query_count, 1, 'make 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing->unload(); $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 after single-object unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing->unload(); $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 after single-object unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 after class unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 after class unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); # Now try with all the objects of a class $query_count = 0; my @things = URT::Thing->get(); is(scalar(@things), 3, 'get() got 3 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'get() got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'got 3 things with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $_->unload() foreach @things; @things = URT::Thing->get(); ok(scalar(@things), 'Got thing with ID 1 after single-object unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $_->unload() foreach @things; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'Got 3 things after single-object unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 3, 'Got 3 things after class unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'Got 3 things after class unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); # Try removing rows from the DB ok($dbh->do('delete from thing where thing_id = 1'), 'delete thing ID 1 from the database directly'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things after delete with get'); is_deeply([sort map { $_->id } @things], [1,2,3], 'Object IDs were correct'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 2, 'reload still returns 3 things'); # ID 1 is gone is_deeply([sort map { $_->id } @things], [2,3], 'Object IDs were correct'); is($query_count, 2, 'Made 2 queries'); # 1 to get all the objects, another to verify ID 1 was gone $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 2, 'After class unload, get() returns 2 things'); is_deeply([sort map { $_->id } @things], [2,3], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing where thing_id = 2'), 'delete thing ID 2 from the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'After delete, reload returns 1 thing'); # ID 1 a and 2 are deleted is_deeply([sort map { $_->id } @things], [3], 'Object IDs were correct'); is($query_count, 2, 'Made 2 queries'); # 1 to get all the objects, another to verify ID 1 was gone $query_count = 0; URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'After delete, reload returns 1 thing'); is_deeply([sort map { $_->id } @things], [3], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do("insert into thing values (4,'orange')"), 'Insert a new row into the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 2, 'After DB insert and class unload, get() returns 2 things'); is_deeply([sort map { $_->id } @things], [3,4], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do("insert into thing values (5,'purple')"), 'Insert a new row into the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'After DB insert, reload returns 3 things'); is_deeply([sort map { $_->id } @things], [3,4,5], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing'), 'delete all rows from the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 0, 'After DB delete and class unload, get() returns 0 things'); is($query_count, 1, 'Made 1 query'); #$DB::single=1; ok($dbh->do("insert into thing values (6,'black')"), 'Insert a new row into the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 1, 'After DB delete and class unload, get() returns 1 thing'); is($things[0]->id, 6, 'Object ID was correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing'), 'again, delete all rows from the database directly'); @things = $cx->reload('URT::Thing'); is(scalar(@things), 0, 'reload returns no things'); URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 0, 'reload returns 0 things after unload'); ok($dbh->do("insert into thing values (7,'brown')"), 'Insert a new row into the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'reload returns 1 thing'); is($query_count, 1, 'Made 1 query'); 50b_get_via_sql.t100664023532023421 1034612544604517 16172 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok( $dbh->do('create table related_thing (thing_id integer not null primary key, name varchar not null)'), 'create related_thing table'); ok( $dbh->do('create table thing (thing_id integer not null primary key, name varchar not null, related_id integer REFERENCES related_thing(thing_id))'), 'create thing table'); my $insert_related = $dbh->prepare('insert into related_thing values (?,?)'); ok($insert_related, 'prepare to insert to related_thing'); $insert_related->execute(11,'red'); $insert_related->execute(12,'blue'); $insert_related->execute(13,'green'); $insert_related->finish(); my $insert_thing = $dbh->prepare('insert into thing values (?,?,?)'); ok($insert_thing, 'prepare to insert to thing'); $insert_thing->execute(1,'pink',11); $insert_thing->execute(2,'cornflower',12); $insert_thing->execute(3,'turquoise',13); $insert_thing->finish(); ok($dbh->commit,'Commit data to DB'); UR::Object::Type->define( class_name => 'URT::RelatedThing', id_by => 'thing_id', has => [ 'name' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related_thing', ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', # not the same as related_thing.thing_id has => [ name => { is => 'String' }, related => { is => 'URT::RelatedThing', id_by => 'related_id' } ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # Do a full join so the IDs returned by the SQL are duplicated my @things = URT::Thing->get(sql => 'select thing.thing_id from thing join related_thing'); is(scalar(@things), 3, 'Got 3 things'); is_deeply([map { $_->id } @things], [1,2,3], 'IDs are correct'); @things = URT::Thing->get(sql => 'select * from thing order by thing_id DESC'); is(scalar(@things), 3, 'Got 3 things'); is_deeply([map { $_->id } @things], [1,2,3], 'IDs are correct'); @things = eval { URT::Thing->get(sql => 'select name from thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: thing_id}s, 'got exception from SQL without primary key'); is(scalar(@things), 0, 'Returned 0 things'); @things = URT::Thing->get(sql => ['select thing_id from thing where name = ?', 'pink']); is(scalar(@things), 1, 'Got 1 thing with name pink using SQL with a placeholder'); is($things[0]->id, 1, 'It was the right ID'); @things = eval { URT::Thing->get(sql => ['select thing_id from thing where name = ? and thing_id = ?', 'pink']) }; like($@, qr{The number of params supplied \(1\) does not match the number of placeholders \(2\)}, 'got exception from SQL without primary key'); is(scalar(@things), 0, 'Returned 0 things'); ok( $dbh->do('create table multi_thing (id1 integer not null, id2 integer not null, name varchar, primary key(id1,id2))'), 'Create table with 2 primary keys'); my $multi_insert = $dbh->prepare('insert into multi_thing values (?,?,?)'); $multi_insert->execute(1,1,'bob'); $multi_insert->execute(1,2,'bob'); $multi_insert->execute(2,1,'fred'); $multi_insert->execute(2,2,'fred'); UR::Object::Type->define( class_name => 'URT::MultiThing', id_by => ['id1', 'id2'], has => ['name'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'multi_thing', ); @things = URT::MultiThing->get(sql => 'select * from multi_thing order by id2'); is(scalar(@things), 4, 'Got 4 items from multi_thing table'); is_deeply([map { $_->id } @things], ["1\t1","1\t2","2\t1","2\t2"], 'Objects returned in the right order'); @things = eval { URT::MultiThing->get(sql => 'select id1 from multi_thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: id2}s, 'got exception from SQL missing one primary key'); @things = eval { URT::MultiThing->get(sql => 'select name from multi_thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: id1, id2}s, 'got exception from SQL missing both primary keys'); 51_get_with_hints.t100664023532023421 1544412544604517 16557 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 50; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #UR::DBI->monitor_sql(1); my $query_count; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created subscription to count queries'); #$DB::single = 1; $query_count = 0; my $thing = URT::Thing->get(name => 'Bob', -hints => [ 'attribs' ]); ok($thing, 'get() returned an object'); is($thing->name, 'Bob', 'object name is correct'); is($thing->thing_id, 1, 'ID is correct'); is($query_count, 1, 'Correctly made 1 query'); $query_count = 0; my @attribs = URT::Attrib->is_loaded(); is(scalar(@attribs), 2, 'The last get() also loaded 2 attribs'); @attribs = sort { $a->attrib_id <=> $b->attrib_id } @attribs; # Just in case, but they should already be in this order... is($query_count, 0, 'Correctly made no queries'); is($attribs[0]->name, 'alignment', 'First attrib name is correct'); is($attribs[0]->value, 'good', 'First attrib value is correct'); is($attribs[1]->name, 'job', 'Second attrib name is correct'); is($attribs[1]->value, 'cook', 'Second attrib value is correct'); $query_count = 0; @attribs = $thing->attribs(); is(scalar(@attribs), 2, 'accessing attribs through the delegated property returned 2 things'); is($query_count, 0, 'Correctly made no queries'); is($attribs[0]->name, 'alignment', 'First attrib name is correct'); is($attribs[0]->value, 'good', 'First attrib value is correct'); is($attribs[1]->name, 'job', 'Second attrib name is correct'); is($attribs[1]->value, 'cook', 'Second attrib value is correct'); $query_count = 0; my $person = URT::Person->get(name => 'Frank', -hints => ['params']); ok($person, 'get() returned an object'); is($person->name, 'Frank', 'object name is correct'); is($person->person_id, 2, 'ID is correct'); is($query_count, 1, 'Correctly made 1 query'); my @bridges = URT::Bridge->is_loaded(); is(scalar(@bridges), 3, '3 bridges were loaded from the above query'); my @params = URT::Param->is_loaded(); is(scalar(@params), 3, '3 params were loaded from the above query'); $query_count = 0; @bridges = $person->bridges(); is(scalar(@bridges), 3, 'got 3 bridges through the delegated accessor'); is($query_count, 0, 'Correctly made no queries'); $query_count = 0; @params = $person->params(); is(scalar(@params), 3, 'got 3 params through the delegated accessor'); is($query_count, 0, 'Correctly made no queries'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # attribs belong to one thing ok( $dbh->do("create table thing (thing_id integer, name varchar)"), 'Created thing table'); ok( $dbh->do("create table attrib (attrib_id integer, name varchar, value varchar, thing_id integer REFERENCES thing(thing_id))"), 'Created attrib table'); my $insert = $dbh->prepare("insert into thing (thing_id, name) values (?,?)"); foreach my $row ( ( [1, 'Bob'], [2, 'Christine']) ) { ok( $insert->execute(@$row), 'Inserted a thing'); } $insert->finish; $insert = $dbh->prepare("insert into attrib (attrib_id, name, value, thing_id) values (?,?,?,?)"); foreach my $row ( ( [1, 'alignment', 'good', 1], [2, 'job', 'cook', 1], [3, 'alignment', 'evil', 2], [4, 'color', 'red', 2] ) ) { ok($insert->execute(@$row), 'Inserted an attrib'); } $insert->finish; # params are many-to-many with people ok( $dbh->do("create table person (person_id integer, name varchar)"), 'created table foo'); ok( $dbh->do("create table param (param_id integer, name varchar, value varchar)"), 'created param table'); ok( $dbh->do("create table person_param_bridge (person_id integer REFERENCES person(person_id), param_id integer REFERENCES param(param_id), PRIMARY KEY (person_id, param_id))" ), 'created bridge table'); $insert = $dbh->prepare("insert into person (person_id, name) values (?,?)"); foreach my $row ( ( [ 1, 'Joe'], [ 2, 'Frank'] )) { ok($insert->execute(@$row), 'inserted a person'); } $insert->finish; $insert = $dbh->prepare("insert into param (param_id, name, value) values (?,?,?)"); foreach my $row ( ( [ 1, 'rank', 'cog' ], [ 2, 'status', 'single' ], [ 3, 'title', 'capn' ], [ 4, 'tag', 'xyzzy' ] )) { ok($insert->execute(@$row), 'inserted a param'); } $insert->finish; $insert = $dbh->prepare("insert into person_param_bridge (person_id, param_id) values (?,?)"); foreach my $row ( ( [ 1, 1 ], [ 2, 1 ], [ 2, 2 ], [ 2, 4 ] )) { ok($insert->execute(@$row), 'inserted a bridge'); } $insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, attribs => { is => 'URT::Attrib', reverse_as => 'thing', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Attrib', id_by => 'attrib_id', has => [ name => { is => 'String' }, value => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'attrib', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => 'name', has_many_optional => [ bridges => { is => 'URT::Bridge', reverse_as => 'persons' }, params => { via => 'bridges', to => 'params' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Param', id_by => 'param_id', has => ['name','value'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'param', ); UR::Object::Type->define( class_name => 'URT::Bridge', id_by => [ 'person_id', 'param_id' ], has => [ persons => { is => 'URT::Person', id_by => 'person_id' }, params => { is => 'URT::Param', id_by => 'param_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person_param_bridge', ); } 51b_unmatched_hints_query_cache.t100664023532023421 1115012544604517 21415 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT; use Test::More tests => 23; # When doing a get that includes a delegated property, and the delegation # does not match anything, make sure a later query correctly does not re-query # the database my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); $dbh->do('create table manufacturer (mfg_id integer NOT NULL PRIMARY KEY, name varchar)'); $dbh->do('create table model (model_id integer NOT NULL PRIMARY KEY, name varchar, mfg_id integer REFERENCES manufacturer(mfg_id))'); my $insert = $dbh->prepare('insert into manufacturer values (?,?)'); ok($insert, 'Insert manufacturers'); foreach my $row ( [1,'Ford'], [2,'Toyota'], [3,'Packard']) { $insert->execute(@$row); } $insert->finish; # Ford has 2 models: Focus and F150 # Toyota has 2 models: Prius and Tundra # Packard and Desoto have no models $insert = $dbh->prepare('insert into model values (?,?,?)'); ok($insert, 'Insert models'); foreach my $row ( [1,'Focus',1], [2,'F150',1], [3,'Prius',2], [4,'Tundra', 2] ) { $insert->execute(@$row); } $insert->finish; UR::Object::Type->define( class_name => 'URT::Manufacturer', data_source => 'URT::DataSource::SomeSQLite', table_name => 'manufacturer', id_by => [ mfg_id => { is => 'integer' }, ], has => [ name => { is => 'String' }, models => { is => 'URT::Model', is_many => 1, reverse_as => 'manufacturer', is_optional => 1 }, #model_ids => { via => 'models', to => 'model_id', is_many => 1, is_optional => 1 }, model_ids => { via => 'models', to => 'model_id', is_many => 1, is_optional => 1 }, ], ); UR::Object::Type->define( class_name => 'URT::Model', table_name => 'model', data_source => 'URT::DataSource::SomeSQLite', id_by => [ model_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, manufacturer => { is => 'URT::Manufacturer', id_by => 'mfg_id' }, manufacturer_name => { via => 'manufacturer', to => 'name' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); # Test a get() with hints $query_count = 0; my @mfg = URT::Manufacturer->get(id => 1, -hints => ['model_ids']); is(scalar(@mfg),1, 'Got 1 manufacturer with id 1'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @models = URT::Model->get(1); # model_id 1 should have been loaded by the above mfg get() is(scalar(@models), 1, 'Get model by id 1 got one object'); is($query_count, 0, 'Made no queries'); $query_count = 0; @models = URT::Model->get(mfg_id => 1); # These should also have been loaded before is(scalar(@models), 2, 'Two models with mfg_id => 1'); is($query_count, 0, 'Made no queries'); # Test a get() with a delegated property $query_count = 0; @mfg = URT::Manufacturer->get(model_ids => 3); is(scalar(@mfg), 1, 'Got 1 manufacturer with model_id 3'); is($mfg[0]->name, 'Toyota', 'Was the right manufacturer'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(model_id => 3); # Should have been loaded by mfg get() with model_id 3 is(scalar(@models), 1, 'Got 1 model with model_id 3'); is($query_count, 0, 'Made no queries'); # test a get() with hints where the hinted property matches nothing $query_count = 0; @mfg = URT::Manufacturer->get(id => 3, -hints => ['model_ids']); is(scalar(@mfg), 1, 'Got 1 manufacturer with id 3'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(mfg_id => 3); # Should have been loaded as part of the mfg get() with id 3 is(scalar(@models), 0, 'Got no models with mfg_id 3'); is($query_count, 0, 'Made no queries'); # This is to avoid an additional query in the next get() when objects are # indexed. It's a side-effect of model_ids being is_many, and the Index # not indexing by is_many properties URT::Model->get(mfg_id => 2); # Test a get() by delegated property that matches nothing $query_count = 0; @mfg = URT::Manufacturer->get(model_ids => 99); is(scalar(@mfg), 0, 'Got no manufacturers with model_id 99'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(model_id => 99); is(scalar(@models), 0, 'Got no models with model_id 99'); SKIP: { skip "via properties don't record info in all_params_loaded yet", 1; is($query_count, 0, 'Made no queries'); } 1; 52_limit_cache_size.t100664023532023421 1157512544604517 17035 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 25; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #UR::DBI->monitor_sql(1); is(UR::Context->object_cache_size_highwater(50), 50, 'Set the max cache size to 50'); is(UR::Context->object_cache_size_lowwater(25), 25, 'Set the lowwater mark to 25'); # get a thing and hold onto a reference to it my $thing = URT::Thing->get(thing_id => 1); ok($thing, 'Got thing_id 1'); is( &count_things_in_cache(), 1, 'There is one object in the cache'); # Ask for an object that doesn't exist my $not_thing = URT::Thing->get(thing_id => 99999); ok(! $not_thing, 'get() for object that does not exist'); is( &count_things_in_cache(), 1, 'Still one object in the cache'); # We'll hold on to these, too my @keep_datas = $thing->datas(); is(scalar(@keep_datas), 2, 'Loaded 2 hangoff datas for that thing'); is( &count_things_in_cache(), 3, 'There are three objects in the cache'); my @things = URT::Thing->get(thing_id => { operator => '<=', value => '50'} ); is(scalar(@things), 50, 'Loaded 50 things with ID <= 50'); is(&count_things_in_cache('URT::Data'), 2, '2 URT::Datas are still in the cache'); is( &count_things_in_cache(), 52, 'There are 52 objects in the cache'); @things = URT::Thing->get(thing_id => { operator => '>', value => '80'} ); is(scalar(@things), 19, 'loaded 19 things with thing_id > 80'); is( &count_things_in_cache(), 22, 'The new 19 things, plus the original thing and 2 datas are still in the cache'); $thing = undef; is( &count_things_in_cache(), 21, 'After letting go of the original thing, there are now 21 objects in the cache'); $thing = URT::Thing->is_loaded(thing_id => 1); ok(!$thing, 'URT::Thing id 1 is no longer loaded'); @things = (); my @datas = URT::Data->get(id => { operator => '>', value => '80'}); is(scalar(@datas), 19, 'Loaded 19 datas with id > 80'); is(&count_things_in_cache('URT::Data'), 21, 'In total, there are 21 datas in the cache'); is(&count_things_in_cache('URT::Thing'), 19, 'Those 19 things are still loaded'); @datas = (); @keep_datas = (); is(&count_things_in_cache('URT::Data'), 19, 'After letting go of the original 2 datas, there are now 19 loaded'); $thing = URT::Thing->get(thing_id => 1); ok($thing, 're-got thing_id 1 after it was purged from the cache'); $thing = undef; @things = URT::Thing->get(); is(scalar(@things), 99, 'Got all URT::Things'); @things = (); &count_things_in_cache(); @datas = URT::Data->get(); is(scalar(@datas), 99, 'Got all URT::Datas'); @things = URT::Thing->is_loaded(); is(scalar(@things), 0, '0 URT::Things are loaded now'); @datas = (); &count_things_in_cache(); @things = URT::Thing->get(); is(scalar(@things), 99,'re-got all URT::Things after they were purged from the cache'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # attribs belong to one thing $dbh->do("create table thing (thing_id integer, name varchar)"); $dbh->do("create table hangoff (id integer, data varchar, thing_id integer REFERENCES thing(thing_id))"); my $insert = $dbh->prepare("insert into thing (thing_id, name) values (?,?)"); for (my $i = 1; $i < 100; $i++) { $insert->execute($i, $i); } $insert->finish; # Two of these hangoffs will be related to one thing $insert = $dbh->prepare("insert into hangoff (id, data, thing_id) values (?,?,?)"); for (my $i = 1; $i < 100; $i++) { my $thing_id = int(($i+1)/2); $insert->execute($i, $i, $thing_id); } $insert->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, datas => { is => 'URT::Data', reverse_as => 'thing', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Data', id_by => 'id', has => [ data => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'hangoff', ); } sub count_things_in_cache { my $count = 0; my @classes; if (@_) { @classes = @_; } else { @classes = ( 'URT::Thing', 'URT::Data' ); } foreach my $c ( @classes ) { # my $this = scalar(values %{$UR::Context::all_objects_loaded->{$c}}); # print "Found $this $c objects\n"; # foreach (values %{$UR::Context::all_objects_loaded->{$c}} ) { # print "\tid ",$_->id,"\n"; # } $count += scalar(grep { defined } values %{$UR::Context::all_objects_loaded->{$c}}); } return $count; } 53_abandoned_iterator.t100664023532023421 703412544604517 17342 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 15; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my $iter = URT::Thing->create_iterator(thing_value => { operator => '<', value => 15}); my @objects; for (my $i = 1; $i < 10; $i++) { push @objects, $iter->next(); } is(scalar(@objects), 9, 'Loaded 9 objects through the (still open) iterator'); my @objects2 = URT::Thing->get(thing_value => { operator => '<', value => 15 } ); is(scalar(@objects2), 14, 'get() with same params loads all relevant objects from the DB'); $iter = undef; @objects2 = URT::Thing->get(thing_value => { operator => '<', value => 15 } ); is(scalar(@objects2), 14, 'get() with same params loads all relevant objects from the DB after undeffing the iterator'); URT::Thing->unload(); $iter = undef; $iter = URT::Thing->create_iterator(); ok($iter, 'Created iterator with no filters'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; # Now try to get all the objects @objects2 = URT::Thing->get(); is(scalar(@objects2), 19, 'get() with no filters returns all the objects after undefining the iterator'); URT::Thing->unload(); $iter = URT::Thing->create_iterator(thing_value => { operator => 'like', value => '%1%' }); ok($iter, 'Created iterator with filter on thing_value'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; @objects2 = URT::Thing->get(thing_value => { operator => 'like', value => '%1%' }); is(scalar(@objects2), 11, 'get() with the same filter on thing_value returns all the objects'); URT::Thing->unload(); $iter = URT::Thing->create_iterator(thing_one => 1); ok($iter, 'Created iterator with filter on thing_one'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; @objects2 = URT::Thing->get(thing_one => 1); is(scalar(@objects2), 19, 'get() with the same filter on thing_one returns all the objects'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, thing_value integer, thing_one integer)"), 'Created thing table'); my $insert = $dbh->prepare("insert into thing (thing_id, thing_value, thing_one) values (?,?,1)"); for (my $i = 1; $i < 20; $i++) { unless($insert->execute($i,$i)) { ok(0, 'Failed in insert test data to DB'); exit; } } $insert->finish; ok(1, 'Inserted test data to DB'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ thing_value => { is => 'Integer' }, thing_one => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 54_valid_values.t100664023532023421 1073512544604517 16217 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 25; class Game::Card { has => [ suit => { is => 'Text', valid_values => [qw/heart diamond club spade/], }, color => { is => 'Text', valid_values => [qw/red blue green/], is_mutable => 0 }, owner => { is => 'Text', is_optional => 1 }, pips => { is => 'Integer', is_optional => 0 }, ], }; for my $class (qw/Game::Card/) { my $c1 = $class->create(suit => 'spade', color => 'red', pips => 4); ok($c1, "created an object with a valid property"); my @i1 = $c1->__errors__; is(scalar(@i1), 0, "no cases of invalididy") or diag(Data::Dumper::Dumper(\@i1)); my $c2 = $class->create(suit => 'badsuit', color => 'blue', pips => 9); ok($c2, "created an object with an invalid property"); my $pips_is_integer = $class->__meta__->properties(property_name => 'pips', data_type => 'Integer'); ok($pips_is_integer, 'pips is Integer (not Number) so Integer checks are performed'); my $c5 = $class->create(suit => 'heart', color => 'blue', pips => '0 but true'); ok($c5, "created an object with an invalid property"); my @i5 = $c5->__errors__; is(scalar(@i5), 0, 'got no errors on c5 object'); my $c6 = $class->create(suit => 'heart', color => 'blue', pips => '0buttrue'); ok($c6, "created an object with an invalid property"); my @i6 = $c6->__errors__; is(scalar(@i6), 1, 'got one error on c6 object'); is($i6[0]->type, 'invalid', 'got an invalid error on c6 object'); is(($i6[0]->properties)[0], 'pips', 'got an invalid error for `pips` on c6 object'); my @i2 = $c2->__errors__; is(scalar(@i2), 1, "one expected cases of invalididy") or diag(Data::Dumper::Dumper(\@i2)); is($i2[0]->__display_name__, qq(INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade), 'Error text is corect'); $c2->suit('heart'); @i2 = $c2->__errors__; is(scalar(@i2), 0, "zero cases of invalididy after fix") or diag(Data::Dumper::Dumper(\@i2)); my $c3 = $class->create(suit => 'spade', color => 'red'); ok($c3, 'Created color with missing required param'); my @i3 = $c3->__errors__; is(scalar(@i3), 1, 'one expected cases of invalididy') or diag(Data::Dumper::Dumper(\@i3)); is($i3[0]->__display_name__, qq(INVALID: property 'pips': No value specified for required property), 'Error text is corect'); my $c4 = $class->create(suit => 'badsuit', color => 'blue'); ok($c4, 'Created object with invalid property value and missing required param'); my @i4 = sort { $a->__display_name__ cmp $b->__display_name__ } $c4->__errors__; is(scalar(@i4), 2, 'two expected cases of invalididy') or diag(Data::Dumper::Dumper(\@i4)); is($i4[0]->__display_name__, qq(INVALID: property 'pips': No value specified for required property), 'First error text is corect'); is($i4[1]->__display_name__, qq(INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade), 'second error text is corect'); my $context = UR::Context->current; $context->dump_error_messages(0); $context->queue_error_messages(1); ok(!UR::Context->commit, 'Commit fails as expected'); my @error_messages = sort {$a cmp $b } UR::Context->current->error_messages(); is(scalar(@error_messages), 4, 'commit generated 4 error messages'); is($error_messages[-1], # This one prints first, but is last 'Invalid data for save!', 'First error message is correct'); my $c4_id = $c4->id; like($error_messages[-2], qr/Game::Card identified by $c4_id has problems on\s+INVALID: property 'pips': No value specified for required property\s+INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade\s+Current state:\s+\$VAR1 = bless\( {/s, 'Second error message is correct'); my $c3_id = $c3->id; like($error_messages[-3], qr/Game::Card identified by $c3_id has problems on\s+INVALID: property 'pips': No value specified for required property\s+Current state:\s+\$VAR1 = bless\( {/s, 'Third error message is correct'); } 55_on_the_fly_metadb.t100664023532023421 732012544604517 17160 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use File::Temp; use Sub::Install; use strict; use warnings; # This test assummes the storage DB schema already exists, but that the metaDB has # no record of it. plan tests => 20; # Make a data source # There is a bug w/ the file temp path generated on Mac OS X and SQLite. # Fix, and restore this to use File::Temp. my $db_file = '/tmp/pid' . $$; END { unlink $db_file; } # Hrm... seems that 'server' still isn't a proper property yet IO::File->new($db_file,'w')->close(); sub URT::DataSource::OnTheFly::server { return $db_file } my $ds = UR::Object::Type->define( class_name => 'URT::DataSource::OnTheFly', is => 'UR::DataSource::SQLite', ); ok($ds, 'Defined data source'); # Connect to the datasource's DB directly, create a couple of tables and some seed data my $dbh = URT::DataSource::OnTheFly->get_default_handle; ok($dbh->do('create table TABLE_A (a_id integer PRIMARY KEY, a_value varchar)'), 'Created TABLE_A'); ok($dbh->do('create table TABLE_B (b_id integer PRIMARY KEY, a_id int references TABLE_A(a_id))'), 'Created TABLE_B'); ok($dbh->do("insert into TABLE_A (a_id, a_value) values (10,'hello')"), 'Inserted row into table_a'); ok($dbh->do("insert into TABLE_B (b_id, a_id) values (2,10)"), 'Inserted row into table_b'); ok($dbh->commit(), 'Inserts committed to the DB'); # Define a couple of classes to go with those tables # Note that we're not going to insert anything in the MetaDB about # these tables my $class_a = UR::Object::Type->define( class_name => 'URT::ClassA', id_by => ['a_id'], has => [ a_value => { is => 'Text' }, ], data_source => $ds->id, table_name => 'TABLE_A' ); ok($class_a, 'Defined ClassA'); my $class_b = UR::Object::Type->define( class_name => 'URT::ClassB', id_by => ['b_id'], has => [ a_obj => { is => 'URT::ClassA', id_by => 'a_id' }, ], data_source => $ds->id, table_name => 'TABLE_B', ); ok($class_b, 'Defined ClassB'); # Now interact with the object API to get/create/save data my @results; @results = URT::ClassA->get(10); ok(scalar(@results) == 1, 'We can get an item from ClassA'); @results = URT::ClassB->get(2); ok(scalar(@results) == 1, 'We can get an item from ClassB'); @results = URT::ClassB->get(1); ok(scalar(@results) == 0, 'Get ClassB with non-existent ID correctly returns 0 items'); my $new_a = URT::ClassA->create(a_value => 'there'); ok($new_a, 'We are able to create a new ClassA item'); my $new_b = URT::ClassB->create(a_id => $new_a->a_id); ok($new_b, 'We are able to create a new ClassB item'); ok(UR::Context->commit(), 'Committed to the DB successfully'); # Check that the data made it to the DB my $sth = $dbh->prepare('select * from table_a order by a_id'); ok($sth, 'select on table_a prepared'); $sth->execute(); my $results = $sth->fetchall_arrayref(); is(scalar(@$results), 2, 'There are 2 rows in table_a'); is_deeply($results, [[$new_a->id, 'there'], [10, 'hello']], 'Data in table_a is correct'); $sth = $dbh->prepare('select * from table_b order by b_id'); ok($sth, 'select on table_b prepared'); $sth->execute(); $results = $sth->fetchall_arrayref(); is(scalar(@$results), 2, 'There are 2 rows in table_b'); is_deeply($results, [[$new_b->b_id, $new_a->a_id],[2,10]], 'Data in table_a is correct'); 55b_partial_metada_data.t100664023532023421 744312544604517 17626 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; # This test assummes the storage DB schema already exists, but that the metaDB has # incomplete or outdated info about it, though the class and actual DB schema do match plan tests => 26; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('create table TABLE_A (a_id integer PRIMARY KEY, value1 varchar, value2 varchar)'), 'Create table'); ok($dbh->do("insert into TABLE_A values (1,'hello','there')"), 'insert row 1'); ok($dbh->do("insert into TABLE_A values (2,'goodbye','cruel world')"), 'insert row 2'); ok(UR::Object::Type->define( class_name => 'URT::A', id_by => 'a_id', has => ['value1','value2'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'TABLE_A', ), 'Define class A'); # Fab up metaDB info, but leave out the value2 column my %table_info = ( data_source => 'URT::DataSource::SomeSQLite', owner => 'main', table_name => 'TABLE_A'); #my %table_info = ( data_source => 'URT', owner => 'main', table_name => 'TABLE_A'); ok(UR::DataSource::RDBMS::Table->__define__(%table_info, last_object_revision => time(), er_type => 'entity', table_type => 'table'), 'Make table metadata obj'); ok(UR::DataSource::RDBMS::TableColumn->__define__(%table_info, last_object_revision => time(), column_name => 'a_id', data_type => 'integer', nullable => 'N'), 'Make column metadata obj for a_id'); ok(UR::DataSource::RDBMS::TableColumn->__define__(%table_info, last_object_revision => time(), column_name => 'value1', data_type => 'varchar', nullable => 'Y'), 'Make column metadata obj for value1'); ok(UR::DataSource::RDBMS::PkConstraintColumn->__define__(%table_info, column_name => 'a_id', rank => 0), 'Make Pk constraint metadata obj for a_id'); my $obj = URT::A->get(1); ok($obj, 'Got object with ID 1'); my %values = ( a_id => 1, value1 => 'hello', value2 => 'there'); foreach my $key ( keys %values ) { is($obj->$key, $values{$key}, "$key property is correct"); } ok($obj->value2('gracie'), 'Change value for value2'); $obj = URT::A->get(2); ok($obj, 'Got object with ID 2'); %values = ( a_id => 2, value1 => 'goodbye', value2 => 'cruel world'); foreach my $key ( keys %values ) { is($obj->$key, $values{$key}, "$key property is correct"); } ok($obj->delete, 'Delete object ID 2'); $obj = URT::A->create(a_id => 3, value1 => 'it', value2 => 'works'); ok($obj, 'Created a new object'); ok(UR::Context->current->commit, 'Commit'); my $sth = $dbh->prepare('select * from table_a where a_id = ?'); ok($sth, 'Make statement handle for checking data'); $sth->execute(1); my $objdata = $sth->fetchrow_hashref(); ok($objdata, 'Got data for a_id == 1'); is_deeply($objdata, { a_id => 1, value1 => 'hello', value2 => 'gracie'}, 'Saved data is correct'); $sth->execute(2); $objdata = $sth->fetchrow_hashref(); ok(!$objdata, 'Data for a_id == 2 was deleted'); $sth->execute(3); $objdata = $sth->fetchrow_hashref(); ok($objdata, 'Got data for a_id == 3'); is_deeply($objdata, { a_id => 3, value1 => 'it', value2 => 'works'}, 'Saved data is correct'); 56_order_by_returns_items_in_order.t100664023532023421 2566512544604517 22224 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 31; # When a different ordering is requested, make sure a get() that hits # the DB returns items in the same order as one that returns cached objects. # It should be sorted first by the requested key, then by ID &setup_classes_and_db(); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); my @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; my @expected = ( { id => 5, name => 'Bobs', data => 'aaa' }, { id => 2, name => 'Bob', data => 'abc' }, { id => 4, name => 'Bobby', data => 'abc' }, { id => 6, name => 'Bobert', data => 'infinity' }, { id => 1, name => 'Bobert', data => 'zzz' }, { id => 0, name => 'Bobbb', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Bob% ordered by data'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Bob% ordered by data from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Now do descending @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Try order by -id $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-id']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 16, name => 'Fredert', data => 'infinity' }, { id => 15, name => 'Freds', data => 'aaa' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by id DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['-id']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by id DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Now, try multiple order bys $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['+data','name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 15, name => 'Freds', data => 'aaa' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data,name from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # multiple, different order bys $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','-name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 15, name => 'Freds', data => 'aaa' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','-name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # different order bys in the other order $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # And now both descending $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','-name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','-name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); # Inserting them purposfully in non-ID order so they'll get returned in non-id # order if the ID column isn't included in the 'order by' clause foreach my $row ( ( [4, 'Bobby', 'abc'], [2, 'Bob', 'abc'], [0, 'Bobbb', undef], [1, 'Bobert', 'zzz'], [6, 'Bobert', 'infinity'], [5, 'Bobs', 'aaa'], [14, 'Freddy', 'abc'], [12, 'Fred', 'abc'], [10, 'Freddd', undef], [11, 'Fredert', 'zzz'], [16, 'Fredert', 'infinity'], [15, 'Freds', 'aaa'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ 'name' => { is => 'String' }, 'data' => { is => 'String', is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 56b_order_by_calculated_property.t100664023532023421 533012544604517 21612 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; # When a different ordering is requested, make sure a get() that hits # the DB returns items in the same order as one that returns cached objects. # It should be sorted first by the requested key, then by ID my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); # Inserting them purposfully in non-ID order so they'll get returned in non-id # order if the ID column isn't included in the 'order by' clause foreach my $row ( ( [4, 'Bobby', 'abc'], [2, 'Bob', 'abc'], [1, 'Bobert', 'zzz'], [6, 'Bobert', 'infinity'], [5, 'Bobs', 'aaa'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, uc_name => { is => 'String', calculate_from => ['name'], calculate => q( uc($name) ) }, data => { is => 'String' }, uc_data => { is => 'String', calculate_from => ['data'], calculate => q( uc($data) ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['uc_data']); is(scalar(@o), 5, 'Got 2 things with name like Bob% ordered by uc_name'); my @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; my @expected = ( { id => 5, name => 'Bobs', data => 'aaa' }, { id => 2, name => 'Bob', data => 'abc' }, { id => 4, name => 'Bobby', data => 'abc' }, { id => 6, name => 'Bobert', data => 'infinity' }, { id => 1, name => 'Bobert', data => 'zzz' }, ); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Bob%', -order => ['uc_data']); is(scalar(@o), 5, 'Got 2 things with name like Bob% ordered by data'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); 56c_via_property_with_order_by.t100664023532023421 1716712544604517 21357 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 5; # A thing has many attributes, which are ordered and have names UR::Object::Type->define( class_name => 'Thing', id_by => 'thing_id', has_many => [ attribs => { is => 'Attribute', reverse_as => 'thing', }, favorites => { via => 'attribs', where => [ key => 'favorite', '-order_by' => 'rank' ], to => 'value', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ); UR::Object::Type->define( class_name => 'Attribute', id_by => 'attrib_id', has => [ thing => { is => 'Thing', id_by => 'thing_id' }, key => { is => 'String' }, rank => { is => 'Integer' }, value => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribs', ); subtest 'in database' => sub { plan tests => 3; my $thing_id = 99; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('create table things (thing_id integer NOT NULL PRIMARY KEY)'), 'create table things'); ok($dbh->do('create table attribs (attrib_id integer NOT NULL PRIMARY KEY, thing_id INTEGER REFERENCES things(thing_id), key VARCHAR NOT NULL, rank INTEGER NOT NULL, value VARCHAR)'), 'create table attributes'); $dbh->do("insert into things values ($thing_id)"); my $insert = $dbh->prepare('insert into attribs values (?,?,?,?,?)'); foreach my $val ( [ 99, $thing_id, 'favorite', 4, 4 ], [ 100, $thing_id, 'favorite', 2, 2 ], [ 101, $thing_id, 'favorite', 6, 6 ], [ 102, $thing_id, 'favorite', 2, 2 ], [ 103, $thing_id, 'favorite', 1, 1 ], [ 104, $thing_id, 'name', 1, 'Fred' ], ) { $insert->execute(@$val); } my $thing = Thing->get($thing_id); my @favorites = $thing->favorites(); is_deeply( \@favorites, [ 1, 2, 2, 4, 6], 'Got back ordered favorites'); }; subtest 'in-memory' => sub { plan tests => 1; my $thing = Thing->create(); Attribute->create(thing_id => $thing->id, key => 'name', value => 'Bob', rank => 1); Attribute->create(thing_id => $thing->id, key => 'favorite', value => $_, rank => $_) foreach (qw( 4 2 6 2 1 )); my @favorites = $thing->favorites(); is_deeply( \@favorites, [1, 2, 2, 4, 6], 'Got back ordered favorites'); }; subtest '"to" is via-to' => sub { plan tests => 1; UR::Object::Type->define( class_name => 'ViaThing', has_many => [ attribs => { is => 'ViaAttribute', reverse_as => 'thing' }, favorites => { via => 'attribs', to => 'value', where => [ key => 'favorite', '-order_by' => 'rank' ] }, ], ); UR::Object::Type->define( class_name => 'ViaValue', has => [ 'value' ], ); UR::Object::Type->define( class_name => 'ViaAttribute', has => [ thing => { is => 'ViaThing', id_by => 'thing_id' }, key => { is => 'String' }, value_obj => { is => 'ViaValue', id_by => 'value_obj_id' }, value => { via => 'value_obj', to => 'value' }, rank => { is => 'Integer' }, ] ); # make a Thing with favorites 1, 2, 4 and 6, ranked in numerical order # but their IDs are not sorted the same as their values/ranks my $thing = ViaThing->create(); my @value_objs = map { ViaValue->create(value => $_) } (qw( 4 2 6 2 1)); my @attrib_objs = map { ViaAttribute->create( thing_id => $thing->id, key => 'favorite', value_obj_id => $_->id, rank => $_->value, ) } @value_objs; my @favorites = $thing->favorites; is_deeply(\@favorites, [ 1, 2, 2, 4, 6], 'Got back ordered favorites', ); }; subtest '"to" is id-class-by' => sub { plan tests => 1; UR::Object::Type->define( class_name => 'IdClassByThing', has_many => [ attribs => { is => 'IdClassByAttribute', reverse_as => 'thing' }, favorite_objs => { via => 'attribs', to => 'value_obj', where => [ key => 'favorite', '-order_by' => 'rank' ] }, ], ); UR::Object::Type->define( class_name => 'IdClassByValue', has => 'value', ); UR::Object::Type->define( class_name => 'IdClassByAttribute', has => [ thing => { is => 'IdClassByThing', id_by => 'thing_id' }, key => { is => 'String' }, value_obj => { is => 'IdClassByValue', id_by => 'value_obj_id', id_class_by => 'value_obj_class' }, value => { via => 'value_obj', to => 'value' }, rank => { is => 'Integer' }, ], ); # make a Thing with favorites 1, 2, 4 and 6, ranked in numerical order # but their IDs are not sorted the same as their values/ranks my $thing = IdClassByThing->create(); my @value_objs = map { IdClassByValue->create(value => $_) } ( qw( 4 2 6 2 1)); my @attrib_objs = map { IdClassByAttribute->create( thing_id => $thing->id, key => 'favorite', value_obj_id => $_->id, value_obj_class => $_->class, rank => $_->value, ) } @value_objs; my @favorite_obj_values = map { $_->value } $thing->favorite_objs; is_deeply(\@favorite_obj_values, [ 1, 2, 2, 4, 6 ], 'Got back ordered favorites'); }; subtest '"to" is id-by' => sub { plan tests => 1; UR::Object::Type->define( class_name => 'IdByThing', has_many => [ attribs => { is => 'IdByAttribute', reverse_as => 'thing' }, favorite_objs => { via => 'attribs', to => 'value_obj', where => [ key => 'favorite', '-order_by' => 'rank' ] }, ], ); UR::Object::Type->define( class_name => 'IdByValue', has => 'value', ); UR::Object::Type->define( class_name => 'IdByAttribute', has => [ thing => { is => 'IdByThing', id_by => 'thing_id' }, key => { is => 'String' }, value_obj => { is => 'IdByValue', id_by => 'value_obj_id' }, value => { via => 'value_obj', to => 'value' }, rank => { is => 'Integer' }, ], ); # make a Thing with favorites 1, 2, 4 and 6, ranked in numerical order # but their IDs are not sorted the same as their values/ranks my $thing = IdByThing->create(); my @value_objs = map { IdByValue->create(value => $_) } ( qw( 4 2 6 2 1)); my @attrib_objs = map { IdByAttribute->create( thing_id => $thing->id, key => 'favorite', value_obj_id => $_->id, rank => $_->value, ) } @value_objs; my @favorite_obj_values = map { $_->value } $thing->favorite_objs; is_deeply(\@favorite_obj_values, [ 1, 2, 2, 4, 6 ], 'Got back ordered favorites'); }; 57_order_by_merge_new_objects.t100664023532023421 474712544604517 21100 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; # There are 2 items in the DB and one newly created object in the cache # that will satisfy the get(). Make sure they're all returned and in # the order asked for &setup_classes_and_db(); my $newobj = URT::Thing->create(thing_id => -1, name => 'Alan', data => 'baaa'); # The default order is by thing_id which would return them in the # order 'Mike', 'Fred. my @o = URT::Thing->get('data like' => 'ba%', -order => ['name']); is(scalar(@o), 3, 'Got 3 objects with data like ba%'); is($o[0], $newobj, 'First object is the newly created object'); is($o[1]->id, 4, 'Second object id is 4'); is($o[1]->name, 'Bobby', 'Second object name is Bobby'); is($o[1]->data, 'baz', 'Second object data is baz'); is($o[2]->id, 1, 'Third object id is 1'); is($o[2]->name, 'Joe', 'Third object name is Joe'); is($o[2]->data, 'bar', 'Third object data is bar'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [1, 'Joe', 'bar'], [2, 'Bob', 'foo'], [3, 'Fred', 'quux'], [4, 'Bobby', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 58_order_by_merge_changed_objects.t100664023532023421 674412544604517 21700 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 19; # There are 2 things in the DB and one newly created thing that satisfy # the get() request. But one of the DB items has been changed in the # object cache, and sorts in a different order than the order returned by # the DB query &setup_classes_and_db(); # Change something in memory and see if it'll be honored in the results # It now sorts last in the DB, but first in the object cache my $o = URT::Thing->get(2); $o->data('aaaa'); # Create a new thing my $new_obj= URT::Thing->create(name => 'Bobert', data => 'abc'); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); is(scalar(@o), 3, 'Got 3 things with name like Bob%'); is($o[0]->id, 2, 'thing_id == 2 is first in the list'); # The changed thing is($o[0]->name, 'Bob', 'its name is Bob'); is($o[0]->data, 'aaaa', 'its data is foo'); is($o[1], $new_obj, 'Second item in the list is the newly created Thing'); is($o[2]->id, 4, 'thing_id == 4 is third in the list'); is($o[2]->name, 'Bobby', 'its name is Bobby'); is($o[2]->data, 'baz', 'its data is baz'); # This originally sorted first. Change it so it sorts last $o = URT::Thing->get(1); $o->data('zzz'); $new_obj = URT::Thing->create(name => 'Joeseph', data => 'mmm'); # Should find Joey (data => ccc), Joeseph (data mmm) and Joe (data zzz, originally aaa) @o = URT::Thing->get('name like' => 'Joe%', -order => ['data']); is(scalar(@o), 3, 'Got three things with name like Joe%'); is($o[0]->id, 5, 'thing_id == 5 is first in the list'); is($o[0]->name, 'Joey', 'its name is Joey'); is($o[0]->data, 'ccc', 'its data is ccc'); is($o[1], $new_obj, 'Second item in the list is the newly created Thing'); is($o[2]->id, 1, 'thing_id == 1 is third in the list'); # The changed thing is($o[2]->name, 'Joe', 'its name is Joe'); is($o[2]->data, 'zzz', 'its data is zzz'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [1, 'Joe', 'aaa'], [2, 'Bob', 'zzz'], [3, 'Fred', 'quux'], [4, 'Bobby', 'baz'], [5, 'Joey', 'ccc'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 59_get_merge_new_objs_with_db.t100664023532023421 463112544604517 21050 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 5; # This tests the scenario where the Context's loading iterator must # merge objects fulfilling a get() request between objects in # the cache and objects loaded from a DB &setup_classes_and_db(); URT::Thing->create(thing_id => 1, name => 'Bob', data => '1234'); URT::Thing->create(thing_id => 3, name => 'Bob', data => '5678'); my @o = URT::Thing->get(name => 'Bob'); # 2 objects in the DB plus 2 more that we created is(scalar(@o), 4, 'Get returned 4 objects'); my @expected = ( { thing_id => 1, name => 'Bob', data => '1234' }, { thing_id => 2, name => 'Bob', data => 'foo' }, { thing_id => 3, name => 'Bob', data => '5678' }, { thing_id => 4, name => 'Bob', data => 'baz' }, ); my @got = map { { thing_id => $_->thing_id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Data returned is as expected'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 60_get_merge_changed_objs_with_db.t100664023532023421 416512544604517 21642 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; # This tests the scenario where we have several objects in the # DB that fulfills a get() request. But before performing that # get(), we change one of the objects so that it will no longer # match the later get(). &setup_classes_and_db(); my $o = URT::Thing->get(thing_id => 2); $o->name('Fred'); # This shouldn't match the below query anymore my @o = URT::Thing->get(name => 'Bob'); is(scalar(@o), 1, 'Get returned 1 object'); is($o[0]->thing_id, 4, 'its ID is correct'); is($o[0]->name, 'Bob', 'its name is correct'); is($o[0]->data, 'baz', 'its data is correct'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 60_sql_query_hint.t100664023532023421 655112544604517 16565 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Make sure query_hint and join_hint in class metadata appear in the generated SQL use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', select_hint => '/* person hint */', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1, }, primary_car => { is => 'URT::Car', reverse_as => 'owner', where => ['is_primary true' => 1], is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', query_hint => '/* car hint */', # query_hint is an alias for select_hint join_hint => '/* car join hint */', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { my($ds, $method, $query) = @_; $query_text = $query; $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(1); is(scalar(@p), 1, 'Got one person'); like($query_text, qr(/\* person hint \*/), 'Saw the person hint'); @p = URT::Person->get(id => 2, -hint => ['cars']); is(scalar(@p), 1, 'Got a different person'); like($query_text, qr(/\* person hint car join hint \*/), 'Saw both hints'); my @c = URT::Car->get(id => 5); is(scalar(@c), 1, 'Got one car'); like($query_text, qr(/\* car hint \*/), 'Saw the car hint'); 61_iterator.t100664023532023421 751112544604517 15346 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; my $dbh = &setup_classes_and_db(); # This tests creating an iterator and doing a regular get() # for the same stuff, and make sure they return the same things # create the iterator but don't read anything from it yet my $iter = URT::Thing->create_iterator(name => 'Bob'); ok($iter, 'Created iterator for Things named Bob'); my @objs; while (my $o = $iter->next()) { is($o->name, 'Bob', 'Got an object with name Bob'); push @objs, $o; } is(scalar(@objs), 2, '2 Things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,4], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Bob'], [name => 'Joe']]); ok($iter, 'Created an iterator for things named Bob or Joe'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 5, '5 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,4,6,8,10], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Joe', 'id <' => 8], [name => 'Bob', 'id >' => 3]]); ok($iter, 'Created an iterator for a more complicated OR rule'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 2, '2 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [4,6], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Joe', data => 'foo'],[name => 'Bob']], -order => ['-data']); ok($iter, 'Created an iterator for an OR rule with with descending order by'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 3, '3 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,6,4], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[ id => 2 ], [name => 'Bob', data => 'foo']]); ok($iter, 'Created an iterator for an OR rule with two ways to match the same single object'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 1, 'Got one object back from the iterstor'); is_deeply( [ map { $_->id } @objs], [2], 'Gor the right object ID from the iterator'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'], [6, 'Joe', 'foo'], [8, 'Joe', 'bar'], [10, 'Joe','baz'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); return $dbh; } 61_iterator_merge_changed_objs_with_db.t100664023532023421 1205312544604517 22730 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 18; my $dbh = &setup_classes_and_db(); # This tests creating an iterator and doing a regular get() # for the same stuff, and make sure they return the same things # create the iterator but don't read anything from it yet my $iter = URT::Thing->create_iterator(name => 'Bob'); ok($iter, 'Created iterator for Things named Bob'); my $o = URT::Thing->get(thing_id => 2); my @objs = URT::Thing->get(name => 'Bob'); is(scalar(@objs), 2, 'Get returned 2 objects'); my @objs_iter; while (my $obj = $iter->next()) { push @objs_iter, $obj; } is(scalar(@objs_iter), 2, 'The iterator returned 2 objects'); is_deeply(\@objs_iter, \@objs, 'Iterator and get() returned the same things'); # Iterator behavior is undefined when the caller manipulates the objects # matching the iterator's BoolExpr after the iterator's creation, but before # they come off of the iterator. # # In this case, the iterator will only return the one object still matching # the bx when it's next() method is called, but not the thing that didn't # exist when the iterator was created. # Right now objects 6,8 and 10 are named Joe $iter = URT::Thing->create_iterator(name => 'Joe'); ok($iter, 'Created iterator for Things named Joe'); $o = URT::Thing->get(thing_id => 6); $o->name('Fred'); # Change the name so it no longer matches the request $o = URT::Thing->get(thing_id => 10); $o->delete(); # Delete this one @objs = URT::Thing->get(name => 'Joe'); is(scalar(@objs), 1, 'get() returned 1 thing named Joe after changing the other'); ok(URT::Thing->create(thing_id => 99, name => 'JoeJoe', data => 'abc'), 'Make a new thing that matches the iterator BoolExpr'); $o = $iter->next(); is($o->id, 8, 'Second object from iterator is id 8'); is($o->name, 'Joe', 'Second object name is Joe'); $o = $iter->next(); ok(!$o, 'The iterator is done'); # doesn't return the newly created thing # Make an iterator ordered by 'data', and change 'data' for some of the objects # while it's running. # # Note for future developers: The behavior here is a policy decision, not really # a logical or technological one. If the behavior changes in the future, that # might be ok, but it would need to be documented # initially, the order is 99 (abc), 8 (bar), 4 (baz), 2 (foo), 6 (foo) $iter = URT::Thing->create_iterator('id <' => 100, -order => 'data'); ok($iter, 'Create iterator for all things ordered by data'); # The DB query won't see this because the cursor was opened before the update ok($dbh->do("update things set data = 'aaa' where thing_id = 2"), 'Change data to "aaa" for thing 2 in the DB, it now sorts first'); my @objects; # This should fill in 99 and 8 @objects = ($iter->next(), $iter->next()); ok(URT::Thing->get(4)->delete, 'Delete thing id 4 before the iterator returns it'); $o = eval { $iter->next() }; like($@, qr/Attempt to fetch an object which matched.*'thing_id' => ('|)4('|)/s, 'caught exception about deleted thing id 4'); # completely-consistent iterator behaviour would make this one come next URT::Thing->get(6)->data('bas'); # And might make this one come again at the end of the list URT::Thing->get(99)->data('zzz'); push @objects, $o while ($o = $iter->next()); my @expected_ids = (99,8,2,6); my @got_ids = map { $_->id } @objects; is_deeply(\@got_ids, \@expected_ids, 'Objects are in the expected order'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'], [6, 'Joe', 'foo'], [8, 'Joe', 'bar'], [10, 'Joe','baz'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); return $dbh; } 61a_iterator_with_or_boolexpr.t100664023532023421 456412544604517 21161 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 16; use URT::DataSource::SomeSQLite; # This tests a get() with some "or"s in the boolexpr. &setup_classes_and_db(); my $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(id<0) or (name=Diane)', ); ok($boolexpr, 'defined boolexpr'); my $iter = URT::Thing->create_iterator($boolexpr); my $count = 0; my $thing = undef; while(my $next = $iter->next) { $count++; $thing = $next; } is($count, 1, 'found one thing'); is($thing->id, 3, 'is correct object'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Bob) or (id<0)', ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; $thing = undef; while(my $next = $iter->next) { $count++; $thing = $next; } is($count, 1, 'found one thing'); is($thing->id, 1, 'is correct object'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Bob) or (type_id=8)' ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; while($iter->next) { $count++; } is($count, 2, 'found two things'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Christine) or (id>0)' ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; while($iter->next) { $count++; } is($count, 3, 'found all three things (with no duplicates)'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, type_id integer)"), 'Created thing table'); my $ins_thing = $dbh->prepare("insert into thing (thing_id, name, type_id) values (?,?,?)"); foreach my $row ( ( [1, 'Bob',1], [2, 'Christine',2], [3, 'Diane', 8]) ) { ok( $ins_thing->execute(@$row), 'Inserted a thing'); } $ins_thing->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, type_id => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 62_in_not_in_operator.t100664023532023421 1612612544604517 17427 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 44; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my(@things,%things_by_id); @things = URT::Thing->get(value => [1,2,3]); is(scalar(@things), 3, 'Got 3 things from the DB with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'1'}->id, 1, 'Got value 1'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,4,5]); is(scalar(@things), 3, 'Got 3 things from the DB with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'6'}->id, 6, 'Got value 6'); is($things_by_id{'7'}->id, 7, 'Got value 7'); is($things_by_id{'8'}->id, 8, 'Got value 8'); @things = URT::Thing->get(value => [1,2,3]); is(scalar(@things), 3, 'Got 3 things from the cache with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'1'}->id, 1, 'Got value 1'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,4,5]); is(scalar(@things), 3, 'Got 3 things from the cache with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'6'}->id, 6, 'Got value 6'); is($things_by_id{'7'}->id, 7, 'Got value 7'); is($things_by_id{'8'}->id, 8, 'Got value 8'); @things = URT::Thing->get(value => [ 2,3,4 ]); is(scalar(@things), 3, 'Got 3 things from the DB and cache with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'4'}->id, 4, 'Got value 4'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,7,8]); is(scalar(@things), 3, 'Got 3 things from the DB and cache with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'4'}->id, 4, 'Got value 4'); is($things_by_id{'5'}->id, 5, 'Got value 5'); is($things_by_id{'6'}->id, 6, 'Got value 6'); @things = URT::Thing->get('related_values in' => [1,2,3]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values IN 1-3'); @things = URT::Thing->get('related_values in' => [-1,-2,9,10]); is(scalar(@things), 0, 'Got 0 things with related_values in [-1,-2,9,10]'); # All of them will match value 6 @things = URT::Thing->get('related_values in' => [-1, -2, 6]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values IN [-1, -2, 6]'); @things = URT::Thing->get('related_values not in' => [-10,-9,9,99]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values not in [-10,-9,9,99]'); @things = URT::Thing->get('related_values not in' => [4,5]); is(scalar(@things), 8, 'Got 0 things with related_values not in [4,5]'); # all of them have value 7 @things = URT::Thing->get('related_values not in' => [7,100,101]); is(scalar(@things), 8, 'Got 0 things with related_values not in [7,100,101]'); @things = URT::Thing->get('related_values not in' => [1,2,3,4,5,6,7,8]); is(scalar(@things), 0, 'Got 0 things with related_values not in [1,2,3,4,5,6,7,8]'); # Only things 1 and 2 have optional values set @things = URT::Thing->get('related_optional_values in' => [1,2,3]); is(scalar(@things), 2, 'Got 2 things from DB with related_optional_values in 1-3'); @things = URT::Thing->get('related_optional_values in' => [20,4,16]); is(scalar(@things), 2, 'Got 2 things with related_optional_values in [4,16,20]'); @things = URT::Thing->get('related_optional_values in' => [25,26,-2]); is(scalar(@things), 0, 'Got 0 things with related_optional_values in [-2,25,26]'); @things = URT::Thing->get('related_optional_values in' => [19, undef, 5]); is(scalar(@things), 8, 'All 8 things with related_optional_values in [undef, 5,19]'); # objs 1 and 2 will match the "related values is not null" part @things = URT::Thing->get('related_optional_values not in' => [undef, 6, 22]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [undef, 6, 22]'); # 1 and 2 have related values not in 7,8 (1-6, for example). The others (objs 3-8) are NULL and don't match @things = URT::Thing->get('related_optional_values not in' => [7,8]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [7,8]'); # Same here, 1 and 2 have related values not in the list. Others are NULL @things = URT::Thing->get('related_optional_values not in' => [500,501, -22]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [500,501, -22]'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value integer)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($val,$val); } $sth->finish; ok( $dbh->do("create table related (related_id integer NOT NULL PRIMARY KEY, thing_id integer references thing(thing_id), value integer)"), 'created related table'); $sth = $dbh->prepare('insert into related values (?,?,?)'); my $id = 1; foreach my $val ( 1,2,3,4,5,6,7,8 ) { foreach my $thing_id ( 1..8 ) { $sth->execute($id++,$thing_id,$val); } } $sth->finish; ok( $dbh->do("create table related_optional (related_id integer NOT NULL PRIMARY KEY, thing_id integer references thing(thing_id), value integer)"), 'created related_optional table'); $sth = $dbh->prepare('insert into related_optional values (?,?,?)'); $id = 1; foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($id++,1,$val); $sth->execute($id++,2,$val); } $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'Integer' }, ], has_many => [ relateds => { is => 'URT::Related', reverse_as => 'thing' }, related_values => { via => 'relateds', to => 'value' }, ], has_many_optional => [ related_optionals => { is => 'URT::RelatedOptional', reverse_as => 'thing' }, related_optional_values => { via => 'related_optionals', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Related', id_by => 'related_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related', ); UR::Object::Type->define( class_name => 'URT::RelatedOptional', id_by => 'related_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related_optional', ); } 62b_in_not_in_operator.t100664023532023421 265412544604517 17552 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #test quote escaping in IN clauses my @odd_things = URT::Thing->get(value => [map(join("'", $_, $_), (1,3,5,7))]); is(scalar(@odd_things), 4, 'got back four objects'); my @even_things = URT::Thing->get('value not in' => [map(join("'", $_, $_), (1,3,5,7))]); is(scalar(@even_things), 4, 'got back four objects'); my %everything; for my $t (@odd_things, @even_things) { $everything{$t->id} = $t; } is(scalar(keys(%everything)), 8, 'got entire set of things betwixt the odd and even'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value varchar)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($val,$val . "'" . $val); } $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 63_view_text.t100664023532023421 602612544604517 15535 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; eval "use XML::Dumper"; my $TEST_XML = 1; unless ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'} && $INC{'XML/Dumper.pm'}) { $TEST_XML = undef; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1, reverse_as => 'owner' }, favorite_numbers => { is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, buddy => { is => 'Cat', id_by => 'buddy_id', is_optional => 1 }, ] }; my $p = Person->create(id => 1001, name => 'Fester', age => 99, favorite_numbers => [2,4,7]); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(id => 2001, name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(id => 2002, name => 'nestor', age => 8, owner => $p, fluf => 22, buddy => $c1); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); ######### my @toolkits = $TEST_XML ? ( 'xml','text' ) : ( 'text' ); for my $toolkit (@toolkits) { note('view 1: no aspects'); my $pv1 = $p->create_view( toolkit => $toolkit, aspects => [ ] ); ok($pv1, "got an XML view $pv1 for the object $p"); my @a = $pv1->aspects(); is(scalar(@a),0,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); my @an = $pv1->aspect_names(); is("@an","","got expected aspect list @an"); ######### note('view 2: simple aspects'); my $pv2 = $p->create_view( toolkit => $toolkit, aspects => [ 'name', 'age', 'cats', ] ); ok($pv2, "got an XML view $pv2 for the object $p"); @a = $pv2->aspects(); is(scalar(@a),3,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); @an = $pv2->aspect_names(); is("@an","name age cats","got expected aspect list @an"); ######### note('view 3: aspects with properties'); my $pv3 = $p->create_view( toolkit => $toolkit, aspects => [ { name => 'name', label => 'NAME' }, 'age', { name => 'cats', label => 'Kitties', }, ] ); ok($pv3, "got an XML view $pv3 for the object $p"); @a = $pv3->aspects(); is(scalar(@a),3,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); @an = $pv3->aspect_names(); is("@an","name age cats","got expected aspect list @an"); my $s = $pv3->subject; is($s, $p, "subject is the original model object"); #$pv3->show; my $c = $pv3->content; note($c); } done_testing(); 63b_view_with_subviews.t100664023532023421 505512544604517 17616 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; eval "use XML::Dumper"; if ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'} && $INC{'XML/Dumper.pm'}) { plan tests => 11; } else { plan skip_all => 'works only with systems which have XML::LibXML and XML::LibXSLT.pm'; } #use File::Basename; #use lib File::Basename::dirname(__FILE__)."/../.."; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, ] }; my $p = Person->create(name => 'Fester', age => 99); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); my $pv = $p->create_view( toolkit => 'xml', aspects => [ 'name', 'age', { name => 'cats', perspective => 'default', toolkit => 'xml', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ] ); ok($pv, "got an XML view for the person"); my $pv_got_content = $pv->content; ok($pv_got_content, 'Person XML view generated some content'); SKIP: { skip "Need a better way to validate XML output",1; my $pv_expected_xml = ''; is($pv_got_content,$pv_expected_xml,"XML is as expected for the person view"); } my $c1v = $c1->create_view(toolkit => 'text'); ok($c1v, 'Created text view for a cat'); ok($c1v, "got a text view for one of the cats"); my $c1v_expected_text = "Cat '" . $c1->id . "' age: 2 fluf: 11 name: fluffy owner: Person '" . $p->id . "' age: 99 cats: Cat '" . $c1->id . "' (REUSED ADDR) Cat '".$c2->id."' age: 8 fluf: 22 name: nestor owner: Person '".$p->id."' (REUSED ADDR) name: Fester"; my $c1v_got_content = $c1v->content; ok($c1v_got_content, 'Cat text view generated some content'); chomp $c1v_got_content; # Convert all whitespace to a single space $c1v_got_content =~ s/\n/ /mg; $c1v_got_content =~ s/\s+/ /mg; is($c1v_got_content,$c1v_expected_text,"text is as expected for the cat view"); 63c_view_with_subviews.t100664023532023421 1157512544604517 17643 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; # let the developer supply the toolkits to test # or default to all of them # TODO: add html when it stops dying our @toolkits = @ARGV; eval "use XML::LibXML"; eval "use XML::LibXSLT"; eval "use XML::Dumper"; my $TEST_XML = 1; unless ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'} && $INC{'XML/Dumper.pm'}) { $TEST_XML = undef; } use UR; unless (@toolkits) { @toolkits = $TEST_XML ? qw/json xml text/ : qw/json text/; } class Acme { is => 'UR::Namespace' }; ## value data type class Acme::Value::Years { is => 'UR::Value::Number', }; sub Acme::Value::Years::__display_name__ { my $self = shift; return $self->id . ' yrs'; }; ## value data types can be gotten by their identity ## they cannot be created, deleted, or mutated my $age1 = Acme::Value::Years->get(88); is($age1->__display_name__, "88 yrs", "$age1 has id " . $age1->id . " and display name " . $age1->id . " yrs"); my $age2 = Acme::Value::Years->get(22); is($age2->__display_name__, "22 yrs", "$age2 has id " . $age2->id . " and display name " . $age2->id . " yrs"); ## entity data types class Acme::Animal { has => [ name => { is => 'Text' }, age => { is => 'Years' }, ] }; class Acme::Person { is => 'Acme::Animal', has => [ cats => { is => 'Acme::Cat', is_many => 1 }, ] }; class Acme::Cat { is => 'Acme::Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Acme::Person', id_by => 'owner_id' }, owner_age => { is => 'Number', via => 'owner', to => 'age' }, ] }; ## the set of entities of a given set is finite, and can be created, mutated, deleted my $p = Acme::Person->create(name => 'Fester', age => 99, id => 111); ok($p, "made a test person object to have cats"); my $c1 = Acme::Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11, id => 222); ok($c1, "made a test cat 1"); my $c2 = Acme::Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22, id => 333); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); $DB::single = 1; my $cat_set = $p->cat_set(); ok($cat_set, "got a set object representing the test person's set of cats: $cat_set"); ## as we render the person and the cat set, we will show the same aspects for each class my @person_aspects = ( 'name', 'age', { name => 'cats', #perspective => 'default', #toolkit => 'text', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ); my @cat_set_aspects = ( 'id', 'members', #'owner', #'owner_age', ); # render both objects, in a variety of text-based views for my $obj_aspects_pair ( [$p,\@person_aspects], [$cat_set,\@cat_set_aspects] ) { my ($obj, $aspects) = @$obj_aspects_pair; for my $toolkit (@toolkits) { # TODO: add 'html' to this list note("\nVIEW: " . ref($obj) . " as $toolkit...\n \n"); for my $aspect (@$aspects) { if (ref($aspect) eq 'HASH') { $aspect->{toolkit} = $toolkit; $aspect->{perspective} = 'default'; } } diag("Creating view with toolkit $toolkit"); my $view = $obj->create_view( toolkit => $toolkit, aspects => $aspects, ); ok($view, "got an text view for the person"); $DB::single = 1; my $actual_content = $view->content; ok($actual_content, "$toolkit view of " . ref($obj) . " generated content"); my $expected_content_path = ref($obj); $expected_content_path =~ s/Acme:://; $expected_content_path =~ s/::/_/g; $expected_content_path = lc($expected_content_path); $expected_content_path = __FILE__ . '.expected.' . $expected_content_path . '.' . $toolkit; # this will cause us to skip missing toolkits w/o failing for now. # when json, xml and html all work remove these 4 lines... unless (-e $expected_content_path) { note("No file at $expected_content_path. Cannot validate:\n$actual_content"); next; }; # this is the _actual_ test for above when all tookits are in place ok(-e $expected_content_path, "path exists to expected content for toolkit $toolkit") or do { diag("No file at $expected_content_path? Cannot validate:\n$actual_content"); next; }; my $expected_content = join('', IO::File->new($expected_content_path)->getlines()); is($actual_content, $expected_content, "content matches!") or eval { # stage a file for debugging, or to upgrade the test IO::File->new(">$expected_content_path.new")->print($actual_content) }; #and note("WORKS ON:\n$actual_content"); } } done_testing(); 63c_view_with_subviews.t.expected.cat_set.json100664023532023421 23712544604517 23765 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t{ "id" : "Acme::Cat/And/owner_id/O:\u001dO:111\u001e", "members" : [ { "id" : "222" }, { "id" : "333" } ] } 63c_view_with_subviews.t.expected.cat_set.text100664023532023421 13412544604517 23774 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tAcme Cat Set Acme::Cat/And/owner_id/O:O:111 members: Acme Cat 222 Acme Cat 333 63c_view_with_subviews.t.expected.cat_set.xml100664023532023421 164112544604517 23634 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t (Acme::Cat::Set owner_id => 111) Set 222 Cat 333 Cat 63c_view_with_subviews.t.expected.person.json100664023532023421 56512544604517 23655 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t{ "age" : "99 yrs", "cats" : [ { "age" : "2 yrs", "fluf" : "11", "name" : "fluffy", "owner" : { "id" : "111" } }, { "age" : "8 yrs", "fluf" : "22 yrs", "name" : "nestor", "owner" : { "id" : "111" } } ], "name" : "Fester" } 63c_view_with_subviews.t.expected.person.text100664023532023421 43412544604517 23663 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tAcme Person 111 name: Fester age: 99 yrs cats: Acme Cat 222 name: fluffy age: 2 yrs fluf: 11 owner: Acme Person 111 (REUSED ADDR) Acme Cat 333 name: nestor age: 8 yrs fluf: 22 yrs owner: Acme Person 111 (REUSED ADDR) 63c_view_with_subviews.t.expected.person.xml100664023532023421 374712544604517 23531 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t 111 Person Fester 99 222 Cat fluffy 2 11 111 Person 333 Cat nestor 8 22 111 Person 63d_delete_view.t100664023532023421 357112544604517 16161 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; eval "use XML::Dumper"; if ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'} && $INC{'XML/Dumper.pm'}) { plan tests => 8; } else { plan skip_all => 'works only with systems which have XML::LibXML and XML::LibXSLT'; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, ] }; my $p = Person->create(name => 'Fester', age => 99); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); my $pv = $p->create_view( toolkit => 'xml', aspects => [ 'name', 'age', { name => 'cats', perspective => 'default', toolkit => 'xml', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ] ); ok($pv, "got an xml view for the person"); my $pv_got_content = $pv->content; my $c1v = $c1->create_view(toolkit => 'xml'); ok($c1v, 'Created xml view for a cat'); ok($c1v, "got a xml view for one of the cats"); my $c1v_got_content = $c1v->content; ok($c1v_got_content, 'Cat xml view generated some content'); UR::Context->current->rollback; 63e_enumerate_available_views.t100664023532023421 177212544604517 21071 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; BEGIN { eval "use XML::LibXML"; eval "use XML::LibXSLT"; eval "use XML::Dumper"; if ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'} && $INC{'XML/Dumper.pm'}) { plan tests => 5; use_ok('UR::Object::View::Default::Xsl', qw/url_to_type type_to_url/); } else { plan skip_all => "Cannot load XML::LibXSLT: $@"; } } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $type = URT::Thingy->__meta__; ok($type, 'got meta-object for URT::Thingy class'); my $view = $type->create_view(perspective => 'available-views', toolkit => 'xml'); isa_ok($view, 'UR::Object::View', 'created view for available views'); my $content = $view->content; ok($content, 'generated content'); my $err = $view->error_message; #errors if views do not have perspective and toolkit set appropriately ok(!$err, 'no errors in view creation'); 64_nullable_foreign_key_handling_on_insert_and_delete.t100664023532023421 2473112544604517 26012 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 81; use URT::DataSource::CircFk; use Data::Dumper; # This test verifies that sql generation is correct for inserts and deletes # on tables with nullable foreign key constraints. For a new object, an # INSERT statement should be returned, with null values in nullable foreign # key columns, and a corresponding UPDATE statement to set foreign key # values after the insert. For object deletion, an UPDATE statement # setting nullable foreign keys to null is expected with the DELETE statement setup_classes_and_db(); my @circular = URT::Circular->get(); my $sqlite_ds = UR::Context->resolve_data_source_for_object($circular[0]); is (scalar @circular, 5, 'got circular objects'); for (@circular){ my $id = $_->id; ok($_->delete, 'deleted object'); my $ghost = URT::Circular::Ghost->get(id=> $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_update_and_delete(@sql), "got separate update and delete statement for deleting circular item w/ nullable foreign key"); } eval{ UR::Context->commit(); }; ok(!$@, "circular deletion committed successfully!"); diag($@) if $@; my @bridges = URT::Bridge->get(); for (@bridges){ my $id = $_->id; ok($_->delete(), 'deleted bridge'); my $ghost = URT::Bridge::Ghost->get(id => $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_delete_only(@sql), "didn't update primary key nullable foreign keys on delete"); } eval{ UR::Context->commit(); }; ok( !$@, 'no commit errors on deleting bridge entries w/ nullable foreign keys primary key' ); diag($@) if $@; my @bridges_check = URT::Bridge->get(); is (scalar @bridges_check, 0, "couldn't retrieve deleted bridges"); my @left = URT::Left->get(id=>[1..5]); my @right = URT::Right->get(); while (my $left = shift @left){ my $right = shift @right; my $bridge = URT::Bridge->create(left_id => $left->id, right_id => $right->id); my @sql = $sqlite_ds->_default_save_sql_for_object($bridge); ok(sql_has_insert_only(@sql), "didn't null insert values for bridge entries nullable, no update statement produced)"); } eval{ UR::Context->commit(); }; ok( !$@, 'no commit errors on recreating bridge entries' ); diag($@) if $@; my @chain = ( URT::Gamma->get(), URT::Beta->get(), URT::Alpha->get()); ok (@chain, 'got objects from alpha, beta, and gamma tables'); is (scalar @chain, 3, 'got expected number of objects'); my $gamma = shift @chain; ok ($gamma->delete, 'deleted_object'); for ("URT::Beta", "URT::Alpha"){ my $obj = shift @chain; my $id = $obj->id; my $class = $_."::Ghost"; ok($obj->delete, 'deleted object'); my $ghost = $class->get(id => $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_update_and_delete(@sql), "got separate update and delete statement for deleting bridge items w/ nullable foreign key"); } eval{ UR::Context->commit(); }; ok(!$@, "no error message on commit: $@"); diag($@) if $@; my @chain2 = (URT::Alpha->get(), URT::Beta->get(), URT::Gamma->get()); ok(!@chain2, "couldn't get deleted chain objects!"); my ($new_alpha, $new_beta, $new_gamma); ok($new_alpha = URT::Alpha->create(id => 101, beta_id => 201), 'created new alpha'); my @alpha_sql = $sqlite_ds->_default_save_sql_for_object($new_alpha); ok($new_beta = URT::Beta->create(id => 201, gamma_id => 301), 'created new beta'); my @beta_sql = $sqlite_ds->_default_save_sql_for_object($new_beta); ok($new_gamma = URT::Gamma->create(id => 301, type => 'test2'), 'created new gamma'); for (\@alpha_sql, \@beta_sql){ ok(sql_has_insert_and_update(@$_), 'got seperate insert and update statements for recreating chained objects'); } eval { UR::Context->commit(); }; ok(!$@, "no error message on commit of new alpha,beta,gamma, would fail due to fk constraints if we weren't using sqlite datasource"); diag($@) if $@; my $check_alpha = URT::Alpha->get(id => 101); is ($check_alpha->beta_id, 201, 'initial null value updated correctly for chain object'); my $check_beta = URT::Beta->get(id => 201); is ($check_beta->gamma_id, 301, 'initial null value updated correctly for chain object'); sub sql_has_delete_only{ my @st = @_; return undef if grep {$_->{sql} =~ /update|insert/i} @st; return undef unless grep {$_->{sql} =~/delete/i} @st; return 1; } sub sql_has_insert_only{ my @st = @_; return undef if grep {$_->{sql} =~ /update|delete/i} @st; return undef unless grep {$_->{sql} =~/insert/i} @st; return 1; } sub sql_has_insert_and_update{ my @st = @_; return undef unless grep {$_->{sql} =~ /insert/i} @st; return undef unless grep {$_->{sql} =~ /update/i} @st; return 1; } sub sql_has_update_and_delete{ my @st = @_; return undef unless grep {my $val = $_; $val->{sql} =~ /delete/i} @st; return undef unless grep {my $val = $_; $val->{sql} =~ /update/i} @st; return 1; } sub setup_classes_and_db { my $dbh = URT::DataSource::CircFk->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table circular (id integer primary key, parent_id integer REFERENCES circular(id))"), 'Created circular table'); ok( $dbh->do("create table left (id integer, right_id integer REFERENCES right(id), right_id2 integer REFERENCES right(id), primary key (id, right_id))"), 'Created left table'); ok( $dbh->do("create table right (id integer primary key, left_id integer REFERENCES left(id), left_id2 integer REFERENCES left(id))"), 'Created right table'); ok( $dbh->do("create table alpha (id integer primary key, beta_id integer REFERENCES beta(id))"), 'Created table alpha'); ok( $dbh->do("create table beta (id integer primary key, gamma_id integer REFERENCES gamma(id))"), 'Created table beta'); ok( $dbh->do("create table gamma (id integer primary key, type varchar)"), 'Created table gamma'); ok( $dbh->do("create table bridge (left_id integer REFERENCES left(id), right_id integer REFERENCES right(id), primary key (left_id, right_id))"), 'Created table bridge'); my $ins_circular = $dbh->prepare("insert into circular (id, parent_id) values (?,?)"); foreach my $row ( [1, 5], [2, 1], [3, 2], [4, 3], [5, 4] ) { ok( $ins_circular->execute(@$row), 'Inserted into circular' ); } $ins_circular->finish; my $ins_left = $dbh->prepare("insert into left (id, right_id, right_id2) values (?,?,?)"); my $ins_right = $dbh->prepare("insert into right (id, left_id, left_id2) values (?,?,?)"); foreach my $row ( ( [1,1,2], [2,2,3], [3,3,4], [4,4,5], [5,5,6]) ) { ok( $ins_left->execute(@$row), 'Inserted into left'); ok( $ins_right->execute(@$row), 'Inserted into right'); } my $ins_bridge_left = $dbh->prepare("insert into left(id) values (?)"); $ins_bridge_left->execute(10); my $ins_bridge_right = $dbh->prepare("insert into right(id) values (?)"); my $ins_bridge = $dbh->prepare("insert into bridge(left_id, right_id) values (?, ?)"); for (11..15){ $ins_bridge_right->execute($_); $ins_bridge->execute(10, $_); } $ins_bridge->finish; $ins_bridge_right->finish; $ins_bridge_left->finish; $ins_left->finish; $ins_right->finish; my $ins_alpha = $dbh->prepare("insert into alpha(id, beta_id) values(?,?)"); ok($ins_alpha->execute(100,200), 'inserted into alpha'); $ins_alpha->finish; my $ins_beta = $dbh->prepare("insert into beta(id, gamma_id) values(?,?)"); ok($ins_beta->execute(200, 300), 'inserted into beta'); $ins_beta->finish; my $ins_gamma = $dbh->prepare("insert into gamma(id, type) values(?,?)"); ok($ins_gamma->execute(300, 'test'), 'inserted into gamma'); $ins_gamma->finish; ok($dbh->commit(), 'DB commit'); ok(UR::Object::Type->define( class_name => 'URT::Circular', id_by => [ id => { is => 'Integer' }, ], has_optional => [ parent_id => { is => 'Integer'}, parent => {is => 'URT::Circular', id_by => 'parent_id'} ], data_source => 'URT::DataSource::CircFk', table_name => 'circular', ), 'Defined URT::Circular class'); ok(UR::Object::Type->define( class_name => 'URT::Left', id_by => [ id => { is => 'Integer'} ], has_optional => [ right_id => { is => 'Integer' }, right => { is => 'URT::Right', id_by => 'right_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'left', ), 'Defined URT::Left class'); ok(UR::Object::Type->define( class_name => 'URT::Right', id_by => [ id => { is => 'Integer'} ], has_optional => [ left_id => { is => 'Integer' }, left => { is => 'URT::Left', id_by => 'left_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'right', ), 'Defined URT::Right class'); ok(UR::Object::Type->define( class_name => 'URT::Alpha', id_by => [ id => {is => 'Integer'} ], has_optional => [ beta_id => { is => 'Integer' }, beta => { is => 'URT::Beta', id_by => 'beta_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'alpha', ), 'Defined URT::Alpha class'); ok(UR::Object::Type->define( class_name => 'URT::Beta', id_by => [ id => {is => 'Integer'} ], has_optional => [ gamma_id => { is => 'Integer' }, gamma => { is => 'URT::Gamma', id_by => 'gamma_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'beta', ), 'Defined URT::Beta class'); ok(UR::Object::Type->define( class_name => 'URT::Gamma', id_by => [ id => {is => 'Integer'} ], has => [ type => { is => 'Text' }, ], data_source => 'URT::DataSource::CircFk', table_name => 'gamma', ), 'Defined URT::Alpha class'); ok(UR::Object::Type->define( class_name => 'URT::Bridge', id_by => [ left_id => {is => 'Integer'}, right_id => {is => 'Integer'} ], data_source => 'URT::DataSource::CircFk', table_name => 'bridge', ), 'Defined URT::Bridge class'); } 65_reload_with_changing_db_data.t100664023532023421 2255712544604517 21345 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 270; use URT::DataSource::SomeSQLite; # This test uses 3 independent groups of classes/tables: # 1) One class where no subclassing is involved (URT::Thing uses table thing) # 2) A pair of classes where we need to do a join to get the subclassed data # URT::Fruit uses the fruit table. URT::Apple is its subclass and uses table # apple # 3) A pair of classes where the child class has no table of its own. # URT::Vehicle uses table vehicle. URT::Car is its subclass and has no table my $dbh = &setup_classes_and_db(); # The test messes with the 'value' property/column. This hash maps the class name # with which table contains the 'value' column my %table_for_class = ('URT::Thing' => 'thing', 'URT::Fruit' => 'apple', 'URT::Apple' => 'apple', 'URT::Vehicle' => 'vehicle', 'URT::Car' => 'vehicle', ); # Context exception messages complain about the class the data originally comes from my %complaint_class = ('URT::Thing' => 'URT::Thing', 'URT::Fruit' => 'URT::Apple', 'URT::Apple' => 'URT::Apple', 'URT::Vehicle' => 'URT::Vehicle', 'URT::Car' => 'URT::Vehicle', ); my $obj_id = 1; foreach my $test_class ( 'URT::Thing', 'URT::Fruit', 'URT::Apple', 'URT::Vehicle', 'URT::Car') { #diag("Working on class $test_class"); UR::DBI->no_commit(0); my $test_table = $table_for_class{$test_class}; my $this_pass_obj_id = $obj_id++; my $thing = $test_class->get($this_pass_obj_id); ok($thing, "Got a $test_class object"); is($thing->value, 1, 'its value is 1'); my $cx = UR::Context->current(); ok($cx, 'Got the current context'); # First test. Make no changes and reload the object ok(eval { $cx->reload($thing) }, 'Reloaded object after no changes'); is($@, '', 'No exceptions during reload'); ok(!scalar($thing->__changes__), 'No changes, as expected'); # Next test, Make a change to the database, no change to the object and reload # It should update the object's value to match the newly reloaded DB data ok($dbh->do("update $test_table set value = 2 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 2'); ok(eval { $cx->reload($thing) }, 'Reloaded object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 2, 'its value is now 2'); ok(!scalar($thing->__changes__), 'No changes. as expected'); # make a change to the object, no change to the DB ok($thing->value(3), 'Changed the object value to 3'); is(scalar($thing->__changes__), 1, 'One change, as expected'); ok(eval { $cx->reload($thing) },' Reload object'); is($@, '', 'No exceptions during reload'); is($thing->value, 3, 'Value is still 3'); is(scalar($thing->__changes__), 1, 'Still one change, as expected'); # Make a change to the DB, and the exact same change to the object ok($dbh->do("update $test_table set value = 3 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 3'); ok($thing->value(3), "Changed the object's value to 3"); ok($thing->__changes__, 'Before reloading, object says it has changes'); ok(eval { $cx->reload($thing) },'Reloaded object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 3, 'Value is 3'); ok(! scalar($thing->__changes__), 'After reloading, object says it has no changes'); # Make a change to the DB data, and a different cahange to the object. This should fail ok($dbh->do("update $test_table set value = 4 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 4'); ok($thing->value(5), "Changed the object's value to 5"); ok(! eval { $cx->reload($thing) },'Reloading fails, as expected'); my $message = $@; $message =~ s/\s+/ /gm; # collapse whitespace my $complaint_class = $complaint_class{$test_class}; like($message, qr/A change has occurred in the database for $complaint_class property 'value' on object ID $this_pass_obj_id from '3' to '4'. At the same time, this application has made a change to that value to '5'./, 'Exception message looks correct'); is($thing->value, 5, 'Value is 5'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(6), "Changed the object's value to 6"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 6 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 6'); ok(eval { $cx->reload($thing) },'Reloading object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 6, 'Value is 6'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(7), "Changed the object's value to 7"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 7 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 7'); ok($thing->value(8), 'Changed object value to 8'); ok(eval { $cx->reload($thing) },'Reloading object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 8, 'Value is 8'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(9), "Changed the object's value to 9"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 10 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 10'); ok($thing->value(11), 'Changed object value to 11'); ok(! eval { $cx->reload($thing) },'Reloading fails, as expected'); $message = $@; $message =~ s/\s+/ /gm; # collapse whitespace like($message, qr/A change has occurred in the database for $complaint_class property 'value' on object ID $this_pass_obj_id from '9' to '10'. At the same time, this application has made a change to that value to '11'/, 'Exception message looks correct'); is($thing->value, 11, 'Value is 11'); } sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer PRIMARY KEY, value integer)"), 'created thing table'); ok($dbh->do("create table fruit (thing_id integer PRIMARY KEY, fruitvalue integer) "), 'created fruit table'); ok($dbh->do("create table apple(thing_id integer PRIMARY KEY references fruit(thing_id), value integer)"), 'created apple table'); ok($dbh->do("create table vehicle (thing_id integer PRIMARY KEY, value integer) "), 'created vehicle table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5 ) { # We need one item for each class under test at the top $sth->execute($val,1); } $sth->finish; my $fruitsth = $dbh->prepare('insert into fruit values (?,?)'); ok($fruitsth, 'Prepared fruit insert statement'); my $applesth = $dbh->prepare('insert into apple values (?,?)'); ok($applesth, 'Prepared apple insert statement'); my $vehiclesth = $dbh->prepare('insert into vehicle values (?,?)'); ok($vehiclesth, 'Prepared vehicle insert statement'); foreach my $val ( 1,2,3,4,5 ) { # one item for each class here, too $fruitsth->execute($val,1); $applesth->execute($val,1); $vehiclesth->execute($val,1); } $fruitsth->finish; $applesth->finish; $vehiclesth->finish; ok($dbh->commit(), 'DB commit'); # A class we can load directly UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # A pair of classes, one that inherits from another. The child class # has a table that gets joined sub URT::Fruit::resolve_subclass_name { return 'URT::Apple'; # All are Apples for this test } UR::Object::Type->define( class_name => 'URT::Fruit', sub_classification_method_name => 'resolve_subclass_name', id_by => 'thing_id', has => [ 'fruitvalue' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'fruit', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Apple', is => 'URT::Fruit', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'apple', ); # Another pair of classes. This time, the child class does not have its own table. sub URT::Vehicle::resolve_subclass_name { return 'URT::Car'; # All are Cars for this test } UR::Object::Type->define( class_name => 'URT::Vehicle', sub_classification_method_name => 'resolve_subclass_name', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'vehicle', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Car', is => 'URT::Vehicle', ); return $dbh; } 66_nullable_hangoff_data.t100664023532023421 517512544604517 20005 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 6; &setup_classes_and_db(); my $thing = URT::Thing->create(thing_id => 3, name => 'Fred'); # There's now 1 colorless thing in the DB and one in the object cache my @colorless = URT::Thing->get(color => undef); is(scalar(@colorless), 2, 'Got two colorless things'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name) values (?,?)'); foreach my $row ( ( [1, 'Bob'], [2, 'Joe'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } ok($dbh->do('create table attributes (attr_id integer, thing_id integer, key varchar, value varchar)'), 'Created attributes table'); $insert = $dbh->prepare('insert into attributes (attr_id, thing_id, key, value) values (?,?,?,?)'); foreach my $row ( ( [1, 1, 'color', 'green'], [2, 1, 'address', '1234 Main St'], [3, 2, 'address', '2345 Oak St'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'attributes': $DBI::errstr"; } } $dbh->commit(); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, attributes => { is => 'URT::Attribute', reverse_as => 'thing', is_many => 1 }, color => { is => 'String', via => 'attributes', to => 'value', where => [key => 'color'], is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); ok(UR::Object::Type->define( class_name => 'URT::Attribute', id_by => 'attr_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, key => { is => 'String' }, value => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'attributes'), 'Created class URT::Attribute'); } 67_composite_id_with_id_class_by_rt55121.t100664023532023421 201112544604517 22660 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; UR::Object::Type->define( class_name => 'Acme::Composited::Polygon', id_by => [ qw/size color shape/ ] ); UR::Object::Type->define( class_name => 'Acme::Box', has_abstract_constant => [ 'subject_class_name' ], has => [ subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being boxed' } ] ); my ($obj,$box); $obj = Acme::Composited::Polygon->create( size => 'big', color => 'blue', shape => 'square' ); ok($obj,'make the composited id object'); $box = Acme::Box->create( subject_class_name => 'Acme::Composited::Polygon' ); ok($box,'make the container'); ok($box->subject($obj),'set subject on container'); ok($box->subject,'container still has subject'); 68_trapped_death_does_not_stack_trace.t100664023532023421 103012544604517 22553 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; $ENV{UR_STACK_DUMP_ON_DIE}=1; sub a { b(@_) } sub b { c(@_) } sub c { d(@_) } sub d { die 'expected' } eval { &a }; if ($@) { note $@; if ($@ =~ /main::b\(\) called/g) { fail('got a stack trace in eval'); } else { pass('looks good'); } } else { fail('$@ wasnt set'); } #&a; 69_subclassify_by.t100664023532023421 3353312544604517 16571 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 102; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); note('Tests for subclassing by regular property'); our $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Employee', subclassify_by => 'subclass_name', is_abstract => 1, has => [ name => { type => "String" }, subclass_name => { type => 'String' }, ], ); UR::Object::Type->define( class_name => 'Acme::Employee::Worker', is => 'Acme::Employee', ); UR::Object::Type->define( class_name => 'Acme::Employee::Boss', is => 'Acme::Employee', ); my $e1 = eval { Acme::Employee->create(name => 'Bob') }; ok(! $e1, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Can't use undefined value as a subclass name/, 'The exception was correct'); $e1 = Acme::Employee->create(name => 'Bob', subclass_name => 'Acme::Employee::Worker'); ok($e1, 'Created an object from the base class and specified subclass_name'); isa_ok($e1, 'Acme::Employee::Worker'); is($e1->name, 'Bob', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Worker', 'subclass_name is correct'); $e1 = Acme::Employee::Worker->create(name => 'Bob2'); ok($e1, 'Created an object from a subclass without subclass_name'); isa_ok($e1, 'Acme::Employee::Worker'); is($e1->name, 'Bob2', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Worker', 'subclass_name is correct'); $e1 = Acme::Employee->create(name => 'Fred', subclass_name => 'Acme::Employee::Boss'); ok($e1, 'Created an object from the base class and specified subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = Acme::Employee::Boss->create(name => 'Fred2'); ok($e1, 'Created an object from a subclass without subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred2', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = Acme::Employee::Boss->create(name => 'Fred3', subclass_name => 'Acme::Employee::Boss'); ok($e1, 'Created an object from a subclass and specified the same subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred3', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = eval { Acme::Employee::Worker->create(name => 'Joe', subclass_name => 'Acme::Employee') }; ok(! $e1, 'Creating an object from a subclass with the base class as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee\) does not match the class it was called on \(Acme::Employee::Worker\)/, 'Exception was correct'); $e1 = eval { Acme::Employee::Worker->create(name => 'Joe', subclass_name => 'Acme::Employee::Boss') }; ok(! $e1, 'Creating an object from a subclass with another subclass as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee::Boss\) does not match the class it was called on \(Acme::Employee::Worker\)/, 'Exception was correct'); $e1 = eval { Acme::Employee::Boss->create(name => 'Joe', subclass_name => 'Acme::Employee::Worker') }; ok(! $e1, 'Creating an object from a subclass with another subclass as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee::Worker\) does not match the class it was called on \(Acme::Employee::Boss\)/, 'Exception was correct'); $e1 = eval { Acme::Employee->create(name => 'Mike', subclass_name => 'Acme::Employee::NonExistent') }; ok(! $e1, 'Creating an object from the base class and gave invalid subclass_name did not work'); like($@, qr/Class Acme::Employee::NonExistent is not a subclass of Acme::Employee/, 'Exception was correct'); note('Tests for default value subclassing'); UR::Object::Type->define( class_name => 'Acme::Tool', is_abstract => 1, subclassify_by => 'subclass_name', has => [ sku => { is => 'Number'}, subclass_name => { is => 'String', default_value => 'Acme::Tool::Generic' }, ], ); UR::Object::Type->define( class_name => 'Acme::Tool::Hammer', is => 'Acme::Tool', ); UR::Object::Type->define( class_name => 'Acme::Tool::Generic', is => 'Acme::Tool', ); my $t = eval { Acme::Tool->create(sku => 123) }; ok($t, 'Created an Acme::Tool without subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name took the default value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool->create(sku => 234, subclass_name => 'Acme::Tool::Generic') }; ok($t, 'Created an Acme::Tool with subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 456) }; ok($t, 'Created an Acme::Tool::Generic without subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 456, subclass_name => 'Acme::Tool::Generic') }; ok($t, 'Created an Acme::Tool::Generic with subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 567, subclass_name => 'Acme::Tool::Broken') }; ok(! $t, 'Did not create an Acme::Tool::Generic with a non-matching subclass_name'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Tool::Broken\) does not match the class it was called on \(Acme::Tool::Generic\)/, 'Exception was correct'); $t = eval { Acme::Tool->create(sku => 678, subclass_name => 'Acme::Tool::Hammer') }; ok($t, 'Created an Acme::Tool with subclass_name Acme::Tool::Hammer'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Hammer', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Hammer'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Hammer->create(sku => 789, subclass_name => 'Acme::Tool::Hammer') }; ok($t, 'Created an Acme::Tool::Hammer with subclass_name Acme::Tool::Hammer'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Hammer', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Hammer'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Hammer->create(sku => 678, subclass_name => 'Acme::Tool::Generic') }; ok(! $t, 'Did not create an Acme::Tool::Hammer with a non-matching subclass_name'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Tool::Generic\) does not match the class it was called on \(Acme::Tool::Hammer\)/, 'Exception was correct'); note('Tests for indirect property subclassing'); UR::Object::Type->define( class_name => 'Acme::Rank', has => [ name => { is => 'String' }, soldier_subclass => { is_calculated => 1, calculate => q( return 'Acme::Soldier::'.ucfirst($self->name) ) }, ] ); UR::Object::Type->define( class_name => 'Acme::Soldier', is_abstract => 1, subclassify_by => 'subclass_name', has => [ name => { is => 'String' }, rank => { is => 'Acme::Rank', id_by => 'rank_id' }, subclass_name => { via => 'rank', to => 'soldier_subclass' }, ], ); UR::Object::Type->define( class_name => 'Acme::Soldier::Private', is => 'Acme::Soldier', ); UR::Object::Type->define( class_name => 'Acme::Soldier::General', is => 'Acme::Soldier', ); my $private = Acme::Rank->create(name => 'Private'); my $general = Acme::Rank->create(name => 'General'); is($private->soldier_subclass, 'Acme::Soldier::Private', 'Private Rank returns correct soldier subclass'); is($general->soldier_subclass, 'Acme::Soldier::General', 'General Rank returns correct soldier subclass'); my $s = eval { Acme::Soldier->create(name => 'Pyle') }; ok(!$s, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Infering a value for property 'subclass_name' via rule.*returned multiple values/, 'Exception is correct'); $s = eval { Acme::Soldier->create(name => 'Pyle', rank => $private) }; ok($s, 'Created object from abstract parent, subclassed via an indirect object property'); is($s->subclass_name, 'Acme::Soldier::Private', 'subclass_name is correct'); isa_ok($s, 'Acme::Soldier::Private'); $s = eval { Acme::Soldier->create(name => 'Pyle', rank_id => $private->id) }; ok($s, 'Created object from abstract parent, subclassed via an indirect object ID'); is($s->subclass_name, 'Acme::Soldier::Private', 'subclass_name is correct'); isa_ok($s, 'Acme::Soldier::Private'); $s = Acme::Soldier->create(name => 'Pyle', subclass_name => 'Acme::Soldier::Private'); ok($s, 'Created object from abstract parent with subclass_name'); isa_ok($s, 'Acme::Soldier::Private'); is($s->rank, $private, 'Rank object was filled in properly'); $s = Acme::Soldier::Private->create(name => 'Beetle'); ok($s, 'Created object from child class'); isa_ok($s, 'Acme::Soldier::Private'); is($s->rank_id, $private->id, 'Its rank_id points to the Private Rank object'); $s = eval { Acme::Soldier::Private->create(name => 'Patton', rank => $general) }; ok(! $s, 'Unable to create an object from a child class when its rank indicates a different subclass'); like($@, qr/Conflicting values for property 'rank_id'/, 'Exception is correct'); note('Tests for calculated subclassing'); # First, setup a table we'll use in the next section of tests... my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do(q(create table vehicle (vehicle_id integer NOT NULL PRIMARY KEY, color varchar NOT NULL, wheels integer NOT NULL))); $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Vehicle', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'vehicle_id', has => [ color => { is => 'String' }, wheels => { is => 'Integer' }, subclass_name => { calculate_from => ['wheels'], calculate => sub { my $wheels = shift; $calculate_called = 1; no warnings 'uninitialized'; if (! defined $wheels) { return; } elsif ($wheels == 2) { return 'Acme::Motorcycle'; } elsif ($wheels == 4) { return 'Acme::Car'; } elsif ($wheels == 0) { return 'Acme::Sled'; } else { die "Can't create a vehicle with $wheels wheels"; } }, }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'vehicle', ); UR::Object::Type->define( class_name => 'Acme::Motorcycle', is => 'Acme::Vehicle', ); UR::Object::Type->define( class_name => 'Acme::Car', is => 'Acme::Vehicle', ); UR::Object::Type->define( class_name => 'Acme::Sled', is => 'Acme::Vehicle', ); $calculate_called = 0; my $v = eval { Acme::Vehicle->create(color => 'blue') }; ok(! $v, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Class Acme::Vehicle subclassify_by calculation property 'subclass_name' requires 'wheels' in the create\(\) params/, 'Exception was correct'); ok(! $calculate_called, 'The calculation function was called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'blue', wheels => 2, subclass_name => 'Acme::Motorcycle'); ok($v, 'Created an object from the base class by specifying subclass_name'); isa_ok($v, 'Acme::Motorcycle'); ok(! $calculate_called, 'The calculation function was not called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'green', wheels => 3, subclass_name => 'Acme::Motorcycle'); ok($v, 'Created another object from the base class'); isa_ok($v, 'Acme::Motorcycle'); ok(! $calculate_called, 'The calculation function was not called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'red', wheels => 4); ok($v, 'Created an object from the base class by specifying wheels'); isa_ok($v, 'Acme::Car'); ok($calculate_called, 'The calculation function was called'); $calculate_called = 0; is($v->subclass_name, 'Acme::Car', "It's subclass_name property is filled in"); ok(! $calculate_called, "Reading the subclass_name property didn't call the calculation sub"); note('Tests for loading with calculated subclassing'); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (99, 'blue', 2))); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (98, 'green', 3))); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (97, 'red', 4))); $calculate_called = 0; $v = Acme::Vehicle->get(99); ok($v, 'Get an Acme::Vehicle out of the DB'); ok($calculate_called, 'The calculation function was called'); isa_ok($v, 'Acme::Motorcycle'); $calculate_called = 0; $v = eval { Acme::Vehicle->get(98) }; ok(! $v, 'Acme::Vehicle with 3 wheels failed to load'); ok($calculate_called, 'The calculation function was called'); like($@, qr/Can't create a vehicle with 3 wheels/, 'Exception was correct'); 69_subclassify_by_db.t100664023532023421 1165212544604517 17234 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 41; my $dbh = URT::DataSource::SomeSQLite->get_default_handle;; ok($dbh, 'Got database handle'); # Employees are subclassed into eith Workers or Bosses. # workers have no additional table, but bosses do ok($dbh->do('create table EMPLOYEE (employee_id integer NOT NULL PRIMARY KEY, name varchar NOT NULL, subclass_name varchar NOT NULL)'), 'create employee table'); ok($dbh->do('create table BOSS (boss_id integer NOT NULL PRIMARY KEY REFERENCES employee(employee_id), office varchar)'), 'create boss table'); # odd numbered employees are workers, evens are bosses my $insert_emp = $dbh->prepare('insert into employee values (?,?,?)'); my $insert_boss = $dbh->prepare('insert into boss values (?,?)'); foreach my $id ( 1 .. 10 ) { if ($id % 2) { # odd $insert_emp->execute($id, 'Bob '.$id, 'URT::Worker'); } else { $insert_emp->execute($id, 'Bob '.$id, 'URT::Boss'); $insert_boss->execute($id, $id); } } $insert_emp->finish; $insert_boss->finish; UR::Object::Type->define( class_name => 'URT::Employee', subclassify_by => 'subclass_name', is_abstract => 1, id_by => 'employee_id', has => [ name => { type => "String" }, subclass_name => { type => 'String' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Worker', is => 'URT::Employee', ); UR::Object::Type->define( class_name => 'URT::Boss', is => 'URT::Employee', id_by => 'boss_id', has => [ office => { is => 'String' }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); my @query_text; my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {push @query_text, $_[2]; $query_count++}), 'Created a subscription for query'); @query_text = (); $query_count = 0; my $o = URT::Employee->get(1); ok($o, 'Got employee with id 1'); isa_ok($o,'URT::Worker'); is($query_count, 1, 'Made one query'); like($query_text[0], qr(from EMPLOYEE), 'Query hits the EMPLOYEE table'); unlike($query_text[0], qr(where subclass_name), 'Query does not filter by subclass_name'); unlike($query_text[0], qr(from BOSS), 'Query does not hit the BOSS table'); @query_text = (); $query_count = 0; $o = URT::Worker->get(3); ok($o, 'Got worker with id 3'); isa_ok($o,'URT::Worker'); is($query_count, 1, 'Made one query'); like($query_text[0], qr(from EMPLOYEE), 'Query hits the EMPLOYEE table'); like($query_text[0], qr(EMPLOYEE.subclass_name), 'Query filters by subclass_name'); unlike($query_text[0], qr(from BOSS), 'Query does not hit the BOSS table'); @query_text = (); $query_count = 0; $o = URT::Employee->get(2); ok($o, 'Got employee with id 2'); isa_ok($o,'URT::Boss'); is($query_count, 2, 'Made 2 queries'); like($query_text[0], qr(from EMPLOYEE), 'first query selects from EMPLOYEE table'); unlike($query_text[0], qr(BOSS), 'first query does not touch the BOSS table'); unlike($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'first query does not filter by subclass_name'); like($query_text[1], qr(from BOSS), 'second query selects from the BOSS table'); like($query_text[1], qr(INNER join EMPLOYEE), 'second query joins to the EMPLOYEE table'); unlike($query_text[1], qr(EMPLOYEE.subclass_name = \?), 'second query does not filter by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Boss->get(4); ok($o, 'Got boss with id 4'); isa_ok($o,'URT::Boss'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(from BOSS), 'Query selects from BOSS table'); like($query_text[0], qr(INNER join EMPLOYEE), 'query joins to the EMPLOYEE table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Worker->get(6); ok(!$o, 'Did not find a Worker with id 6'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(from EMPLOYEE), 'query selects from EMPLOYEE table'); unlike($query_text[0], qr(BOSS), 'query does not mention BOSS table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Boss->get(7); ok(!$o, 'Did not find a boss with id 6'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(INNER join EMPLOYEE), 'query joins to EMPLOYEE table'); like($query_text[0], qr(from BOSS), 'query selects from BOSS table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); 70_command_arg_processing.t100664023532023421 1731412544604517 20242 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More tests => 84; # tests parsing of command-line options class Cmd::Module::V1 { is => 'Command::V1', has => [ a_string => { is => 'String' }, a_number => { is => 'Number' }, opt_string => { is => 'String', is_optional => 1 }, opt_number => { is => 'Number', is_optional => 1 }, optnumber => { is => 'Number', is_optional => 1 }, ], has_output => [ a_output => { is => 'Number', calculate => q/ return 3 * 2 /, }, ], }; class Cmd::Module::V2 { is => 'Command::V2', has => [ a_string => { is => 'String' }, a_number => { is => 'Number' }, opt_string => { is => 'String', is_optional => 1 }, opt_number => { is => 'Number', is_optional => 1 }, optnumber => { is => 'Number', is_optional => 1 }, ], has_output => [ a_output => { is => 'Number', calculate => q/ return 3 * 2 /, }, ], }; # Commands dump errors about missing required properties # we don't care about those problems Cmd::Module::V1->dump_error_messages(0); Cmd::Module::V2->dump_error_messages(0); foreach my $the_class ( qw( Cmd::Module::V1 Cmd::Module::V2 )) { my($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah --a-number 123)); is($class,$the_class, 'Parse args got correct class'); is_deeply($params, { a_string => 'blah', a_number => 123 }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string=blah --a-number=123)); is($class,$the_class, 'Parse args got correct class using = in cmdline'); is_deeply($params, { a_string => 'blah', a_number => 123 }, 'Params are correct'); my $errors; ($class,$params,$errors) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah)); is($class,$the_class, 'Parse args got correct class using = in cmdline'); is_deeply($params, { a_string => 'blah'}, 'Params are correct'); my $r = $class->execute(%$params); ok(!$r, "result works"); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class where value contains ='); is_deeply($params, { a_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string=something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with = where value contains ='); is_deeply($params, { a_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with optional param where value contains ='); is_deeply($params, { opt_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string=something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with optional param = where value contains ='); is_deeply($params, { opt_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah --opt-string foo)); is($class,$the_class, 'Parse args got correct class with is_optional item'); is_deeply($params, { a_string => 'blah', opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string foo --opt-number 4)); is($class,$the_class, 'Parse args got correct class with two is_optional items'); is_deeply($params, { opt_number => 4, opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string=foo --opt-number=4)); is($class,$the_class, 'Parse args got correct class with = and two is_optional items'); is_deeply($params, { opt_number => 4, opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv('--opt-string', '', '--opt-number', ''); is($class,$the_class, 'Parse args got correct class with two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string='' --opt-number='')); is($class,$the_class, 'Parse args got correct class with = and two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string="" --opt-number="")); is($class,$the_class, 'Parse args got correct class with = and two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number 4)); is($class,$the_class, 'Parse args got correct class with one optional number'); is_deeply($params, { opt_number => 4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number=4)); is($class,$the_class, 'Parse args got correct class with = and one optional number'); is_deeply($params, { opt_number => 4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number=-422)); is($class,$the_class, 'Parse args got correct class with = and one optional negative number'); is_deeply($params, { opt_number => -422}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number -4)); is($class,$the_class, 'Parse args got correct class with and one optional negative number'); is_deeply($params, { opt_number => -4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--optnumber -422)); is($class,$the_class, 'Parse args got correct class with and one optional negative number'); is_deeply($params, { optnumber => -422}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string -4)); is($class,$the_class, 'Parse args got correct class with and one optional string where value is a negative number'); is_deeply($params, { opt_string => -4}, 'Params are correct'); my @args = qw(--a-string=abc --a-number=123); my @errors; ($class, $params, @errors) = $the_class->resolve_class_and_params_for_argv(@args); is($class, $the_class, 'Parse args got correct class with no a_number parameter'); is(scalar(@errors), 0, "Not specifying a_number doesn't fail"); is_deeply( $params, { a_string => 'abc', a_number => 123 }, 'Params are correct' ); } 70_command_help_text.t100664023532023421 1012612544604517 17223 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 19; BEGIN { $ENV{'ANSI_COLORS_DISABLED'} = 1 } UR::Object::Type->define( class_name => 'Acme::ParentCommand', is => 'Command', has => [ param_a => { is => 'String', is_optional => 1, doc => 'Some documentation for param a' }, param_b => { is => 'String', is_optional => 0, example_values => ['1','2','3'] }, param_c => { is => 'String', doc => 'Parent documentation for param c' }, ], ); UR::Object::Type->define( class_name => 'Acme::ChildCommand', is => 'Acme::ParentCommand', has => [ param_a => { is => 'String', is_optional => 0 }, param_c => { is => 'String', doc => 'Child documentation for param c' }, ], ); sub Acme::ParentCommand::execute { 1; } sub Acme::ChildCommand::execute { 1; } my $usage_string = ''; my $callback = sub { my $self = shift; $usage_string = shift; $usage_string =~ s/\x{1b}\[\dm//g; # Remove ANSI escape sequences for color/underline }; Acme::ParentCommand->dump_usage_messages(0); Acme::ParentCommand->usage_messages_callback($callback); Acme::ChildCommand->dump_usage_messages(0); Acme::ChildCommand->usage_messages_callback($callback); $usage_string = ''; $DB::single = 1; my $rv = Acme::ParentCommand->_execute_with_shell_params_and_return_exit_code('--help'); is($rv, 0, 'Parent command executed'); my %usage = split_by_section($usage_string); like($usage{'USAGE'}, qr(USAGE\s), 'USAGE has header'); like($usage{'USAGE'}, qr(\sacme parent-command\s), 'USAGE has command'); like($usage{'USAGE'}, qr(\s--param-b=\?\s), 'USAGE has --param-b as required'); like($usage{'USAGE'}, qr(\s--param-c=\?\s), 'USAGE has --param-c as required'); like($usage{'USAGE'}, qr(\s\[--param-a=\?\](\s|$)), 'USAGE has --param-a as optional'); like($usage{'REQUIRED ARGUMENTS'}, qr(\sparam-b\s+String), 'Parent help text lists param-b as required'); like($usage{'REQUIRED ARGUMENTS'}, qr(\sparam-c\s+String\s+Parent documentation for param c), 'Parent help text for param c'); like($usage{'OPTIONAL ARGUMENTS'}, qr(\sparam-a\s+String\s+Some documentation for param a), 'Parent help text lists param-a as optional'); unlike($usage{'REQUIRED ARGUMENTS'}, qr(\sparam-a\s+String), 'Parent help text does not list param-a as required'); unlike($usage{'OPTIONAL ARGUMENTS'}, qr(\sparam-b\s+String), 'Parent help text does not list param-b as optional'); $usage_string = ''; $rv = Acme::ChildCommand->_execute_with_shell_params_and_return_exit_code('--help'); is($rv, 0, 'Child command executed'); like($usage_string, qr(USAGE\s+acme child-command --param-a=\?\s+--param-b=\?\s+--param-c=\?), 'Child help text usage is correct'); like($usage_string, qr(param-a\s+String\s+Some documentation for param a), 'Child help text mentions param-a with parent documentation'); like($usage_string, qr(param-b\s+String), 'Child help text mentions param-b'); like($usage_string, qr(param-c\s+String\s+Child documentation for param c), 'Child help text mentions param-c with child documentation'); unlike($usage_string, qr(OPTIONAL ARGUMENTS\s+param-a\s+String), 'Child help text does not list param-a as optional'); my $meta = Acme::ParentCommand->__meta__; my $p_meta_b = $meta->property('param_b'); my $example_values_arrayref = $p_meta_b->example_values; is("@$example_values_arrayref", "1 2 3", "example values are stored"); is(scalar(@$example_values_arrayref), 3, "example value count is as expected"); sub split_by_section { my $usage_string = shift; my @sections = ('USAGE', 'REQUIRED ARGUMENTS', 'OPTIONAL ARGUMENTS', 'DESCRIPTION'); my $section = shift @sections; my $next_section = shift @sections; my %usage; for my $line (split("\n", $usage_string)) { if ($line =~ /$next_section/) { $section = $next_section; $next_section = shift @sections || 'END'; } if ($usage{$section}) { $usage{$section} .= $line; } else { $usage{$section} = $line; } } return %usage; } 70c_command_tree_usage_text.t100664023532023421 317612544604517 20550 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 2; use IO::File; UR::Object::Type->define( class_name => 'URT::ParentCommand', is => 'Command::Tree', ); { no warnings 'once'; $URT::ParentCommand::SUB_COMMAND_MAPPING = { 'command-a' => 'URT::CommandA', 'command-b' => 'URT::CommandB', }; } UR::Object::Type->define( class_name => 'URT::CommandA', is => 'Command::V2', has => [ param_a => { is => 'String', is_optional => 0 }, param_c => { is => 'String', doc => 'Documentation for param c' }, ], doc => 'This is command a', ); UR::Object::Type->define( class_name => 'URT::CommandB', is => 'Command::V2', has => [ param_a => { is => 'String', is_optional => 0 }, param_b => { is => 'String', doc => 'Documentation for param b' }, ], doc => 'This is command b', ); my $buffer = ''; close STDERR; my $stdout = open(STDERR,'>',\$buffer) || die "Can't redirect stdout to a string"; my $rv = URT::ParentCommand->_execute_with_shell_params_and_return_exit_code(); close STDERR; open(STDERR, ">-") || die "Can't dup original stdout: $!"; STDERR->autoflush(1); ok($rv, 'Parent command executes'); $buffer =~ s/\x{1b}.*?m//mg; # Remove ANSI escape sequences for color/underline my $expected = q(Sub-commands for u-r-t parent-command: command-a This is command a command-b This is command b ERROR: Please specify valid params for 'u-r-t parent-command'. ); is($buffer, $expected, 'Output with no params was as expected'); 71_ur_value.t100664023532023421 2460312544604517 15361 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Scalar::Util qw(refaddr); use Test::More tests => 89; require File::Temp; my $s1 = UR::Value::Text->get('hi there'); ok($s1, 'Got an object for string "hi there"'); is($s1->id, 'hi there', 'It has the right id'); my $s2 = UR::Value::Text->get('hi there'); ok($s2, 'Got another object for the same string'); is($s1,$s2, 'They are the same object'); my $s3 = UR::Value::Text->get('something else'); ok($s3, 'Got an object for a different string'); isnt($s1,$s3, 'They are different objects'); my $s4 = UR::Value::Text->get('0'); ok(defined($s4), 'Got an object for the string "0"'); # Note that $s4 stringifies to "0" which is boolean false is($s4->id, '0', 'The ID is correct'); is($s4, '0', 'It stringifies correctly'); my $text = UR::Value::Text->get('metagenomic composition 16s is awesome'); ok($text, 'Got an object for string "metagenomic composition 16s is awesome"'); is($text->id, 'metagenomic composition 16s is awesome', 'Id is correct'); my $capitalized = $text->capitalize; isa_ok($capitalized, 'UR::Value::Text'); is($capitalized->id, 'Metagenomic Composition 16s Is Awesome', 'Capitalized for is "Metagenomic Composition 16s Is Awesome"'); my $camel = $text->to_camel; isa_ok($camel, 'UR::Value::Text'); is($camel->id, 'MetagenomicComposition16sIsAwesome', 'Text To camel case for is "MetagenomicComposition16sIsAwesome"'); my $lemac = $camel->to_lemac; isa_ok($lemac, 'UR::Value::Text'); is($lemac->id, 'metagenomic composition 16s is awesome', 'Camel case to text for is "MetagenomicComposition16sIsAwesome"'); is($lemac, $text, 'Got the same UR::Value::Text object back for camel case to text'); $text->dump_warning_messages(0); $text->queue_warning_messages(1); ok(!$text->to_hash, 'Failed to convert text object "' . $text->id . '"to a hash when does not start with a dash (-)'); my $text_for_text_to_hash = '-aa foo -b1b -1 bar --c22 baz baz -ddd -11 -eee -f -g22g text -1111 --h_h 44 --i-i -5 -j-----j -5 -6 hello -k -l_l-l g a p -m'; like(($text->warning_messages)[0], qr(Can not convert text object with id "metagenomic composition 16s is awesome" to hash), 'Got expected error message from failed conversion'); my $text_to_hash = UR::Value::Text->get($text_for_text_to_hash); ok($text_to_hash, 'Got object for param text'); my $hash = $text_to_hash->to_hash; ok($hash, 'Got hash for text'); is_deeply($hash->id, { aa => 'foo', b1b => '-1 bar', c22 => 'baz baz', ddd => -11, eee => '', f => '', g22g => 'text -1111', h_h => 44, 'i-i' => -5, 'j-----j' => '-5 -6 hello', k => '', 'l_l-l' => 'g a p', m => '', }, 'Text to hash id is correct'); is($hash->__display_name__, "aa => 'foo',b1b => '-1 bar',c22 => 'baz baz',ddd => '-11',eee => '',f => '',g22g => 'text -1111',h_h => '44',i-i => '-5',j-----j => '-5 -6 hello',k => '',l_l-l => 'g a p',m => ''", 'Hash display name'); my $hash_to_text = $hash->to_text; ok($hash_to_text, 'Got hash to text'); is($hash_to_text, '-aa foo -b1b -1 bar -c22 baz baz -ddd -11 -eee -f -g22g text -1111 -h_h 44 -i-i -5 -j-----j -5 -6 hello -k -l_l-l g a p -m', 'Hash to text is correct'); my $s1_refaddr = Scalar::Util::refaddr($s1); ok($s1->unload(), 'Unload the original string object'); isa_ok($s1, 'UR::DeletedRef'); isa_ok($s2, 'UR::DeletedRef'); $s1 = UR::Value::Text->get('hi there'); ok($s1, 're-get the original string object'); is($s1->id, 'hi there', 'It has the right id'); isnt(Scalar::Util::refaddr($s1), $s1_refaddr, 'It is not the original object reference'); UR::Object::Type->define( class_name => 'Test::Value', is => 'UR::Value', id_by => [ string => { is => 'Text' } ] ); eval { Test::Value->get() }; like($@, qr/Can't load an infinite set of Test::Value/, 'Getting infinite set of Test::Values threw an exception'); my $x1 = Test::Value->get('xyz'); ok($x1,"get('xyz') returned on first call"); my $x2 = Test::Value->get('xyz'); ok($x2,"get('xyz') returned on second call"); is($x1, $x2, 'They were the same object'); my $a1 = Test::Value->get(string => 'abc'); ok($a1,"get(string => 'abc') returned on first call"); my $a2 = Test::Value->get(string => 'abc'); ok($a2,"get(string => 'abc') returned on second call"); is($a1, $a2, 'They were the same object'); my $n1 = Test::Value->get('123'); ok($n1, "get('123') returned on first call"); my $n2 = Test::Value->get(string => '123'); ok($n2,"get(string => '123') returned on second call"); is($n1, $n2, 'They were the same object'); my @o = Test::Value->get(['xyz','abc','123','456']); is(scalar(@o), 4, 'Got 4 Test::Values in a single get()'); is_deeply([ map { $_->id} @o], ['123','456','abc','xyz'], 'Values were returned in ID order'); my %o = map { $_->id => $_ } @o; is($o{'123'}, $n1, "Object with id '123' is the same as the one from earlier"); is($o{'abc'}, $a1, "Object with id 'abc' is the same as the one from earlier"); is($o{'xyz'}, $x1, "Object with id 'xyz' is the same as the one from earlier"); is($o{'456'}->string, '456', 'The 4th value in the last get() constructed the correct object'); UR::Object::Type->define( class_name => 'Test::Value2', is => 'UR::Value', id_by => [ string1 => { is => 'Text' }, string2 => { is => 'Text' }, ], has => [ other_prop => { is => 'Text' }, ], ); eval { Test::Value2->get(string1 => 'abc') }; like($@, qr/Can't load an infinite set of Test::Value2/, 'Getting infinite set of Test::Value2s threw an exception'); $a1 = Test::Value2->get(string1 => 'qwe', string2 => undef); ok($a1, "get(string1 => 'qwe', string2 => undef) worked"); $a2 = Test::Value2->get(id => 'qwe'); ok($a2, "get(id => 'qwe') worked"); is($a1, $a2, 'They were the same object'); $a1 = Test::Value2->get(string1 => 'abc', string2 => 'def'); ok($a1, 'get() with both ID properties worked'); my $sep = Test::Value2->__meta__->_resolve_composite_id_separator; $a2 = Test::Value2->get('abc' . $sep . 'def'); ok($a2, 'get() with the composite ID property worked'); is($a1, $a2, 'They are the same object'); is($a1->other_prop, undef, 'The non-id property is undefined'); $x1 = Test::Value2->get(string1 => 'xyz', string2 => 'xyz', other_prop => 'hi there'); ok($x1, 'get() including a non-id property worked'); is($x1->other_prop, 'hi there', 'The non-id property has the right value'); local $SIG{'__WARN__'} = sub {}; # Suppress warnings about is_unique during boolexpr construction @o = Test::Value2->get(['xyz'.$sep.'xyz', 'abc'.$sep.'abc']); is(scalar(@o), 2, 'get() with 2 composite IDs worked'); { local $SIG{'__WARN__'} = sub {}; # Suppress warnings about is_unique during boolexpr construction eval { Test::Value2->get(id => ['xyz'.$sep.'xyz', 'abc'.$sep.'abc'], other_prop => 'somethign else') }; like($@, qr/Cannot load class Test::Value2 via UR::DataSource::Default when 'id' is a listref and non-id properties appear in the rule/, 'Getting with multiple IDs and including non-id properites threw an exception'); } do { do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); isa_ok($path, 'UR::Value::FilesystemPath', 'path'); is($path, $pathname, 'comparing path object to string works'); }; do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); $path .= 'a'; $pathname .= 'a'; isa_ok($path, 'UR::Value::FilesystemPath', 'after concatenation path still'); is($path, $pathname, 'string concatenation works'); }; do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); like($path, qr/foo/, 'matching works'); }; }; do { # file test "operators" my $temp_file = File::Temp->new(); ok(-f $temp_file, 'created temp_file'); my $temp_dir = File::Temp->newdir(); ok(-d $temp_dir, 'created temp_dir'); my $temp_filename = $temp_file->filename; my $temp_dirname = $temp_dir->dirname; my $symlink_filename_a = $temp_dirname . '/symlink_a'; symlink($temp_filename, $symlink_filename_a); ok(-l $symlink_filename_a, 'created symlink'); do { # file my $path = UR::Value::FilePath->get($temp_filename); isa_ok($path, 'UR::Value::FilesystemPath', 'file path'); is($path->exists, 1, 'file path exists'); is($path->is_dir, '', 'file path is not a dir'); is($path->is_file, 1, 'file path is a file'); is($path->is_symlink, '', 'file path is not a symlink'); is($path->size, 0, 'file path size is zero'); system("echo hello > $path"); isnt($path->size, 0, "file path size isn't zero"); is($path->line_count, 1, 'file path has one line'); }; do { # dir my $path = UR::Value::FilesystemPath->get($temp_dirname); isa_ok($path, 'UR::Value::FilesystemPath', 'dir path'); is($path->exists, 1, 'dir path exists'); is($path->is_dir, 1, 'dir path is a dir'); is($path->is_file, '', 'dir path is not a file'); is($path->is_symlink, '', 'dir path is not a symlink'); }; do { # symlink my $path = UR::Value::FilesystemPath->get($symlink_filename_a); isa_ok($path, 'UR::Value::FilesystemPath', 'symlink path'); is($path->exists, 1, ' symlink path exists'); is($path->is_dir, '', ' symlink path is not a dir'); is($path->is_file, 1, ' symlink path is a file'); is($path->is_symlink, 1, ' symlink path is a symlink'); my $symlink_filename_b = "$temp_dirname/symlink_b"; symlink($path, $symlink_filename_b); ok(-l $symlink_filename_b, 'created symlink_b (from an object)'); }; }; do { class TestIterator { has => [ things => { is => 'Integer', is_many => 1, }, ], }; my $o = TestIterator->create(things => [5, 6, 7, 8]); my $i = $o->thing_iterator(); while (my $v = $i->next()) { } is_deeply($o->thing_arrayref, [5, 6, 7, 8], 'items not remove by Value::Iterator'); }; subtest q(regression test for UR::Value::Text->get('')) => sub { plan tests => 4; my $s1 = UR::Value::Text->get(''); isa_ok($s1, 'UR::Value::Text', 'got an'); is($s1->id, '', 'it has the correct id'); my $s2 = UR::Value::Text->get(''); isa_ok($s2, 'UR::Value::Text', 'got another'); is(refaddr($s1), refaddr($s2), 'they are the same object'); }; 71_ur_value_json.t100664023532023421 305212544604517 16365 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::Object::Type->define( is => 'UR::Value::JSON', class_name => 'URT::JSONTestValue', id_by => ['prop_a','prop_b'], ); subtest 'create' => sub { plan tests => 1; my $obj = URT::JSONTestValue->create(prop_a => 'tc_a', prop_b => 'tc_b'); my $expected_id = '{"prop_a":"tc_a","prop_b":"tc_b"}'; is( $obj->id,$expected_id, 'id is expected json (create)'); }; subtest 'get from properties' => sub { plan tests => 1; my $obj = URT::JSONTestValue->get(prop_a => 'tgfp_a', prop_b => 'tgfp_b'); my $expected_id = '{"prop_a":"tgfp_a","prop_b":"tgfp_b"}'; is($obj->id, $expected_id, 'id is expected json (get)'); }; subtest 'get by single id' => sub { plan tests => 2; my $obj = URT::JSONTestValue->get('{"prop_a":"gs_a","prop_b":"gs_b"}'); is($obj->prop_a,'gs_a', 'prop_a matches (single)'); is($obj->prop_b, 'gs_b', 'prop_b matches (single)'); }; subtest 'get by multiple id' => sub { plan tests => 4; my @objs = URT::JSONTestValue->get(id => [ '{"prop_a":"gm1_a","prop_b":"gm1_b"}', '{"prop_a":"gm2_a","prop_b":"gm2_b"}', ]); is($objs[0]->prop_a, 'gm1_a', 'prop_a matches (multiple 1)'); is($objs[0]->prop_b, 'gm1_b', 'prop_b matches (multiple 1)'); is($objs[1]->prop_a, 'gm2_a', 'prop_a matches (multiple 2)'); is($objs[1]->prop_b, 'gm2_b', 'prop_b matches (multiple 2)'); }; 71_ur_value_multiple_id_properties.t100664023532023421 1142012544604517 22215 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 90; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Scalar::Util; UR::Object::Type->define( is => 'UR::Value', class_name => 'URT::InflatableDefaultSerializer', id_by => ['prop_a','prop_b'], ); UR::Object::Type->define( is => 'UR::Value', class_name => 'URT::InflatableCustomSerializer', id_by => ['prop_a', 'prop_b'], ); sub URT::InflatableCustomSerializer::__deserialize_id__ { my($class, $id) = @_; my %h; @h{'prop_a','prop_b'} = split(':', $id); return \%h; } sub URT::InflatableCustomSerializer::__serialize_id__ { my($class, $props) = @_; return join(':', @$props{'prop_a','prop_b'}); } test_create_single(); test_get_single(); test_get_multiple(); sub test_create_single { foreach my $test ( [ 'URT::InflatableDefaultSerializer', "\t" ], [ 'URT::InflatableCustomSerializer', ':' ], ) { my($test_class, $id_sep) = @$test; note("create single $test_class"); $test_class->dump_error_messages(0); # Supress normal errors my @failed_create_params = ( [], [prop_a => 'foo' ], [prop_b => 'foo']); foreach my $params ( @failed_create_params ) { my $o = $test_class->create(@$params); ok(! $o, "Cannot create $test_class object with only ".scalar(@$params).' params'); } $test_class->dump_error_messages(0); # back on my $o = $test_class->create(prop_a => 'foo', prop_b => 'bar'); ok($o, "Created $test_class object with both named parameters"); _properties_match($o, prop_a => 'foo', prop_b => 'bar', id => join($id_sep, 'foo','bar')); my $o2 = $test_class->get($o->id); is( Scalar::Util::refaddr($o), Scalar::Util::refaddr($o2), "re-getting the same $test_class returns the same instance"); _properties_match($o2, prop_a => 'foo', prop_b => 'bar', id => join($id_sep, 'foo','bar')); $o = $test_class->create(id => join($id_sep, 'baz','quux')); ok($o, "Created $test_class object with id"); _properties_match($o, prop_a => 'baz', prop_b => 'quux', id => join($id_sep, 'baz','quux')); $o2 = $test_class->get($o->id); is( Scalar::Util::refaddr($o), Scalar::Util::refaddr($o2), "re-getting the same $test_class returns the same instance"); _properties_match($o2, prop_a => 'baz', prop_b => 'quux', id => join($id_sep, 'baz','quux')); } } sub test_get_single { foreach my $test ( [ 'URT::InflatableDefaultSerializer', "\t" ], [ 'URT::InflatableCustomSerializer', ':' ], ) { my($test_class, $id_sep) = @$test; note("get single $test_class"); my $o = $test_class->get(prop_a => 'a', prop_b => 'b'); ok($o, 'get() with both named parameters'); _properties_match($o, prop_a => 'a', prop_b => 'b', id => join($id_sep, 'a','b')); my $o2 = $test_class->get($o->id); is( Scalar::Util::refaddr($o), Scalar::Util::refaddr($o2), 're-getting the same object returns the same instance'); _properties_match($o, prop_a => 'a', prop_b => 'b', id => join($id_sep, 'a','b')); $o = $test_class->get(id => join($id_sep, 'c','d')); ok($o, 'get InflatableFromId with both named parameters'); _properties_match($o, prop_a => 'c', prop_b => 'd', id => join($id_sep, 'c','d')); $o2 = $test_class->get($o->id); is( Scalar::Util::refaddr($o), Scalar::Util::refaddr($o2), 're-getting the same object returns the same instance'); _properties_match($o, prop_a => 'c', prop_b => 'd', id => join($id_sep, 'c','d')); } } sub test_get_multiple { foreach my $test ( [ 'URT::InflatableDefaultSerializer', "\t" ], [ 'URT::InflatableCustomSerializer', ':' ], ) { my($test_class, $id_sep) = @$test; note("get multiple $test_class"); my @o = $test_class->get(id => [ join($id_sep, 'e','f'), join($id_sep, 'g','h'), join($id_sep, 'i','j'), ]); is(scalar(@o), 3, 'Get 3 objects by composite ID'); _properties_match($o[0], prop_a => 'e', prop_b => 'f', id => join($id_sep, 'e','f')); _properties_match($o[1], prop_a => 'g', prop_b => 'h', id => join($id_sep, 'g','h')); _properties_match($o[2], prop_a => 'i', prop_b => 'j', id => join($id_sep, 'i','j')); } } sub _properties_match { my $o = shift; while(my $prop = shift) { my $val = shift; is($o->$prop, $val, "property $prop"); } } 72_command_name_validation.t100664023532023421 135512544604517 20347 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More; my %tests = ( 'WordWord' => 'word-word', 'Word456Word' => 'word456-word', 'Word456aWord' => 'word456a-word', '456Word' => '456-word', 'Word456' => 'word456', 'WWWord' => 'w-w-word', '456' => '456', ); plan tests => scalar(keys(%tests)); for my $class (keys %tests) { my $self = 'URT::' . $class; UR::Object::Type->define( class_name => $self, is => 'Command', ); my $command_name = $self->command_name_brief($class); is($command_name, $tests{$class}, 'command name for class style: ' . $class); } 73_opts_spec_creation_and_validation.t100664023532023421 405012544604517 22432 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More; use File::Temp; BEGIN { eval "use Getopt::Complete::Cache;"; if ($@ =~ qr(Can't locate Getopt/Complete/Cache.pm in \@INC)) { plan skip_all => 'Getopt::Complete::Cache does not exist on the system'; } else { plan tests => 11; # This should match the number of keys in %tests below use_ok('Getopt::Complete::Cache'); } } my $fh = File::Temp->new(); my $fname = $fh->filename; # Create the file my $cmd = UR::Namespace::Command::Update::TabCompletionSpec->create(classname => 'UR::Namespace::Command', output => $fname); ok($cmd, 'Created command object'); $cmd->dump_error_messages(0); $cmd->dump_warning_messages(0); $cmd->queue_error_messages(1); $cmd->queue_warning_messages(1); ok($cmd->execute(), 'creating ur spec file in tmp'); my @messages = $cmd->warning_messages(); ok(!scalar(@messages), 'executing command generated no warning messages'); @messages = $cmd->error_messages(); is(scalar(@messages), 1, 'executing command generated one error message'); like($messages[0], qr/Command\.pm\.opts is 0 bytes, reverting to previous/, 'Error message was correct'); # Try loading/parsing the file ok(-f $fname, 'Output options file exists'); my $content = join('', $fh->getlines); my $spec = eval $content; is($@, '', 'eval of spec file worked'); # first look for >define, the next item in the list is subcommands for define my $found = 0; for (my $i = 0; $i < @$spec; $i++) { if ($spec->[$i] eq '>define') { $found = 1; $spec = $spec->[$i+1]; last; } } ok($found, 'Found define top-level command data'); $found = 0; for (my $i = 0; $i < @$spec; $i++) { if ($spec->[$i] eq '>namespace') { $found = 1; last; } } ok($found, 'Found define namespace command data'); # Try importing the file my $rv = Getopt::Complete::Cache->import(file => $fname, above => 1, comp_cword => 1); is($rv, 1, 'importing ur spec from tmp'); 74_xsl_view_url_convert.t100664023532023421 153412544604517 20002 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More; BEGIN { eval "use XML::LibXSLT"; if ($@) { plan skip_all => "Cannot load XML::LibXSLT: $@"; } else { plan tests => 11; use_ok('UR::Object::View::Default::Xsl', qw/url_to_type type_to_url/); } } my @ct = qw{ genome/instrument-data Genome::InstrumentData genome Genome genome/foo-bar/baz Genome::FooBar::Baz funky-town FunkyTown funky-town/oklahoma FunkyTown::Oklahoma }; for ( my $i = 0 ; $i + 1 < @ct ; $i += 2 ) { is( url_to_type( $ct[$i] ), $ct[ $i + 1 ], 'url_to_type ' . $ct[$i] ); is( type_to_url( $ct[ $i + 1 ] ), $ct[$i], 'type_to_url ' . $ct[ $i + 1 ] ); } 76_is_many_default_values.t100664023532023421 245712544604517 20251 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Data::Dumper; use Test::More tests => 6; class Spy { has => [ name => { is => 'Text', default_value => 'James Bond', }, aliases => { is => 'Text', is_many => 1, default_value => ['007', 'Bond', 'James Bond'], }, ], }; { # Test Default Values my $spy = Spy->create(); isa_ok($spy, 'Spy'); ok($spy->name eq 'James Bond', "Spy's default name is correct"); my $default_aliases = '007|Bond|James Bond'; my $aliases = join('|', sort($spy->aliases)); #print "Aliases: $aliases\nExpected Aliases: $default_aliases\n"; ok($aliases eq $default_aliases, "Spy's default aliases are correct"); } { # Test Specified Values my $name = 'Margaretha Geertruida (Grietje) Zelle'; my $alias = 'Mata Hari'; my $spy = Spy->create(name => $name, aliases => [$alias]); isa_ok($spy, 'Spy'); ok($spy->name eq $name, "Spy's name is correct"); my $aliases = join('|', sort($spy->aliases)); #print "Aliases: $aliases\nExpected Aliases: $alias\n"; ok($aliases eq $alias, "Spy's aliases are correct"); } { # TODO: Test complex default values involving database bridges? ; } 77_file_undef_value_handling.t100664023532023421 1467012544604517 20710 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; # Test the different ways that File datasources handling of NULL might differ # with the way Perl and UR convert NULL to undef and the various # numeric and string conversions when doing comparisions. We want UR's # object cache to return the same results that a query against the database # would use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 226; use IO::File; use URT::DataSource::SomeFile; my $ds = URT::DataSource::SomeFile->get(); my $filename = $ds->server; my $fh = IO::File->new($filename, O_WRONLY|O_CREAT); ok($fh, 'Got file handle'); my $delim = $ds->delimiter; $fh->print(join($delim, 1,'',''),"\n"); $fh->print(join($delim, 2,'',''),"\n"); ok($fh->close(),'Write file data'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ value => { is => 'Integer', column_name => 'thing_name' }, color => { is => 'String', column_name => 'thing_color' }, ], data_source => 'URT::DataSource::SomeFile', table_name => 'things', ); my @result; # For the equality operator, "value => undef" is converted to SQL as # "value IS NULL", not "value = NULL, so it should return the items foreach my $value ( undef ) { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } foreach my $value ( '') { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } # For other values using the equality operator, it should return nothing foreach my $value ( 0, 1, -1) { operator_returns_object_count('', $value,0); } ## != for non-null values should return both things foreach my $value ( 0, 1, -1) { my @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) loaded 2 items"); @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } # the 'false' operator should return both things, since NULL is false { my @result = URT::Thing->get(value => { operator => 'false', value => '' }); is(scalar(@result), 2, "value is false (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => 'false', value => ''}); is(scalar(@result), 2, "value is false (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) loaded 2 items"); @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } foreach my $operator ( qw( < <= > >= true ) ) { foreach my $value ( undef, 0, "", 1, -1) { operator_returns_object_count($operator,$value,0); last if ($operator eq 'true' or $operator eq 'false'); # true and false don't use the 'value' anyway } } # FIXME - uninitialized warnings here foreach my $operator ( 'like', 'not like' ) { foreach my $value ( undef, '%', '%1', '%1%' ) { operator_returns_object_count($operator, $value, 0) } } # 'in' operator # value => [undef] does SQL to include NULL items operator_returns_object_count('in', [undef], 2); operator_returns_object_count('not in', [undef], 0); foreach my $operator ( '', 'in', 'not in' ) { foreach my $value ( [], [1] ) { operator_returns_object_count($operator, $value, 0); } } # 'between' operator foreach my $value ( [undef, undef], [1,1], [0,1], [-1,0], [-1,-1], [undef, 1], [undef, 0], [undef, -1], [1, undef], [0, undef], [-1, undef] ) { operator_returns_object_count('between', $value, 0); } sub operator_returns_object_count { my($operator,$value,$expected_count) = @_; if (ref($value) eq 'ARRAY' and !$operator) { $operator = 'in'; } my $print_operator = $operator || '=>'; my $print_value; if (! defined $value) { $print_value = '(undef)'; } elsif (length($value) == 0 ) { $print_value = '""'; } elsif (ref($value) eq 'ARRAY') { $print_value = '[' . join(",", map { defined($_) ? "'$_'" : '(undef)' } @$value) . ']'; } else { $print_value = $value; } # Original non-eq-operator syntax @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) returns $expected_count item(s)"); URT::Thing->unload(); # New syntax my $property_string = "value $operator"; @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) returns $expected_count item(s)"); URT::Thing->unload(); } 77_index_undef_value_handling.t100664023532023421 451612544604517 21056 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 9; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table things (thing_id integer, name varchar)"), 'Created things table'); ok( $dbh->do("create table thing_params ( param_id integer, name varchar, value varchar, thing_id integer REFERENCES things(thing_id))"), 'Created params table'); # Bob has the color green, Fred has tracking_number 12345 $dbh->do("insert into things (thing_id, name) values (99, 'Bob')"); $dbh->do("Insert into things (thing_id, name) values (100, 'Fred')"); $dbh->do("Insert into thing_params (param_id, thing_id, name,value) values (1, 99, 'color', 'green')"); $dbh->do("Insert into thing_params (param_id, thing_id, name,value) values (2, 100, 'tracking_number', '12345')"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ name => { is => 'String' }, params => { is => 'URT::ThingParam', is_many => 1, reverse_as => 'thing' }, color => { is => 'String', via => 'params', to => 'value', where => [ name => 'color' ] }, tracking_number => { is => 'String', via => 'params', to => 'value', where => [ name => 'tracking_number' ] }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ); UR::Object::Type->define( class_name => 'URT::ThingParam', id_by => 'param_id', has => [ name => { is => 'String' }, value => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing_params', ); my $thing = URT::Thing->get(color => undef); ok($thing, 'Got thing with no color'); is($thing->name, 'Fred', 'It was the right thing'); my $new_thing = URT::Thing->create(name => 'Joe'); ok($new_thing, 'Created a new object with no color defined'); my $same_thing = URT::Thing->get(name => 'Joe', color => undef); ok($same_thing, 'Got it back by specifying color => undef'); is($new_thing, $same_thing, 'and it was the same object'); 77_sql_undef_value_handling.t100664023532023421 1455312544604517 20570 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; # Test the different ways that SQL's handling of NULL might differ # with the way Perl and UR convert NULL to undef and the various # numeric and string conversions when doing comparisions. We want UR's # object cache to return the same results that a query against the database # would use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 227; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table things (thing_id integer, value integer)"), 'Created things table'); $dbh->do("insert into things (thing_id, value) values (1, NULL)"); $dbh->do("Insert into things (thing_id, value) values (2, NULL)"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ); my @result; # For the equality operator, "value => undef" is converted to SQL as # "value IS NULL", not "value = NULL, so it should return the items foreach my $test ( [ 'undef' => undef ], [ "''" => '' ], ) { # undef and the empty string both mean NULL my($value_as_string, $value) = @$test; @result = URT::Thing->get(value => $value); is(scalar(@result), 2, "value => $value_as_string loaded 2 items"); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, "value => $value_as_string returned all 2 items"); URT::Thing->unload(); # clear object and query cache } # For other values using the equality operator, it should return nothing foreach my $value ( 0, 1, -1) { operator_returns_object_count('', $value,0); } ## != for non-null values should return both things foreach my $value ( 0, 1, -1) { my @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) loaded 2 items"); @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } # the 'false' operator should return both things, since NULL is false { my @result = URT::Thing->get(value => { operator => 'false', value => '' }); is(scalar(@result), 2, "value is false (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => 'false', value => ''}); is(scalar(@result), 2, "value is false (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) loaded 2 items"); @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } foreach my $operator ( qw( < <= > >= true ) ) { foreach my $value ( undef, 0, "", 1, -1) { operator_returns_object_count($operator,$value,0); last if ($operator eq 'true' or $operator eq 'false'); # true and false don't use the 'value' anyway } } # FIXME - uninitialized warnings here foreach my $operator ( 'like', 'not like' ) { foreach my $value ( undef, '%', '%1', '%1%' ) { operator_returns_object_count($operator, $value, 0) } } # Supress messages about null in-clauses. URT::DataSource::SomeSQLite->warning_messages_callback( sub { my ($self,$msg) = @_; if ($msg =~ m/Null in-clause passed/) { $_[1] = undef; } } ); # 'in' operator # value => [undef] does SQL to include NULL items operator_returns_object_count('in', [undef], 2); operator_returns_object_count('not in', [undef], 0); foreach my $operator ( '', 'in', 'not in' ) { foreach my $value ( [], [1] ) { operator_returns_object_count($operator, $value, 0); } } # 'between' operator foreach my $value ( [undef, undef], [1,1], [0,1], [-1,0], [-1,-1], [undef, 1], [undef, 0], [undef, -1], [1, undef], [0, undef], [-1, undef] ) { operator_returns_object_count('between', $value, 0); } sub operator_returns_object_count { my($operator,$value,$expected_count) = @_; if (ref($value) eq 'ARRAY' and !$operator) { $operator = 'in'; } my $print_operator = $operator || '=>'; my $print_value; if (! defined $value) { $print_value = '(undef)'; } elsif (length($value) == 0 ) { $print_value = '""'; } elsif (ref($value) eq 'ARRAY') { $print_value = '[' . join(",", map { defined($_) ? "'$_'" : '(undef)' } @$value) . ']'; } else { $print_value = $value; } # Original non-eq-operator syntax @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) returns $expected_count item(s)"); URT::Thing->unload(); # New syntax my $property_string = "value $operator"; @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) returns $expected_count item(s)"); URT::Thing->unload(); } 78_get_by_subclass_params_load_properly.t100664023532023421 1013512544604517 23207 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; # This tests a get() by subclass specific parameters on a subclass with no table of its own. # The idea is to make sure that queries run with any subclass specific parameters (which can # be stored in hangoff tables or calculated) do not cause the cache to believe it had loaded # more objects of that specific subclass than it actually has. setup_classes_and_db(); my $fido = URT::Dog->get(color => 'black'); ok($fido, 'Got fido by hangoff parameter (color)'); is($fido->name, 'fido', 'Fido has correct name'); is($fido->id, 1, 'Fido has correct id'); my $rex = URT::Dog->get(color => 'brown'); ok($rex, 'Got rex by hangoff parameter (color)'); SKIP: { skip 'Failed to get rex, not testing his properties', 2 if !defined $rex; is($rex->name, 'rex', 'Rex has correct name'); is($rex->id, 2, 'Rex has correct id'); }; $fido = URT::Dog->get(tag_id => 1); ok($fido, 'Got fido by calculated property (tag_id)'); is($fido->name, 'fido', 'Fido has correct name'); is($fido->id, 1, 'Fido has correct id'); $rex = URT::Dog->get(tag_id => 2); ok($rex, 'Got rex by calculated property (tag_id)'); SKIP: { skip 'Failed to get rex, not testing his properties', 2 if !defined $rex; is($rex->name, 'rex', 'Rex has correct name'); is($rex->id, 2, 'Rex has correct id'); }; sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do(q{ create table animal ( animal_id integer, name varchar, subclass varchar)}), 'Created animal table'); ok($dbh->do(q{ create table animal_param ( animal_param_id integer, animal_id integer references animal(animal_id), param_name varchar, param_value varchar)}), 'Created animal_param table'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (1,'fido','URT::Dog')"), 'Inserted fido'); ok($dbh->do("insert into animal_param (animal_param_id, animal_id, param_name, param_value) values (1, 1, 'color', 'black')"), 'Turned fido black'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (2,'rex','URT::Dog')"), 'Inserted rex'); ok($dbh->do("insert into animal_param (animal_param_id, animal_id, param_name, param_value) values (2, 2, 'color', 'brown')"), 'Turned rex brown'); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Animal', id_by => [ animal_id => { is => 'NUMBER', len => 10 }, ], has => [ name => { is => 'Text' }, subclass => { is => 'Text' }, ], has_many_optional => [ params => { is => 'URT::AnimalParam', reverse_as => 'animal', }, ], is_abstract => 1, subclassify_by => 'subclass', data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Dog', is => 'URT::Animal', has => [ tag_id => { calculate_from => [ 'animal_id' ], calculate => q{ return $animal_id; }, }, color => { via => 'params', is => 'Text', to => 'param_value', where => [ param_name => 'color', ], }, ], ); UR::Object::Type->define( class_name => 'URT::AnimalParam', id_by => [ animal_param_id => { is => 'NUMBER' }, ], has => [ animal => { id_by => 'animal_id', is => 'URT::Animal' }, param_name => { is => 'Text' }, param_value => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal_param', ); } 78b_get_by_subclass_property.t100664023532023421 460312544604517 21002 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 10; # This tests a get() by subclass specific parameters on a subclass with no table of its own. # The property is only defined on the subclass, but the data lives in the table referred to # in the parent class my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do(q{ create table animal ( animal_id integer NOT NULL, name varchar NOT NULL, num_legs integer, subclass varchar NOT NULL)}), 'Created animal table'); ok($dbh->do("insert into animal (animal_id, name, subclass, num_legs) values (1,'fido','URT::Dog', 4)"), 'Inserted fido'); ok($dbh->do("insert into animal (animal_id, name, subclass, num_legs) values (2,'woody','URT::Bird', 2)"), 'Inserted woody'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (3,'jaws','URT::Shark')"), 'Inserted jaws'); ok($dbh->commit(), 'DB commit'); # Dogs and birds have legs, sharks don't UR::Object::Type->define( class_name => 'URT::Animal', id_by => [ animal_id => { is => 'NUMBER', len => 10 }, ], has => [ name => { is => 'Text' }, subclass => { is => 'Text' }, ], subclassify_by => 'subclass', data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Dog', is => 'URT::Animal', has_optional => [ num_legs => { is => 'Integer' }, ], ); UR::Object::Type->define( class_name => 'URT::Bird', is => 'URT::Animal', has_optional => [ num_legs => { is => 'Integer', column_name => 'num_legs' }, ], ); UR::Object::Type->define( class_name => 'URT::Shark', is => 'URT::Animal', ); my @animals = URT::Dog->get(num_legs => 3); is(scalar(@animals), 0, 'No dogs with 3 legs'); @animals = URT::Bird->get(num_legs => 2); is(scalar(@animals), 1, 'Got 1 bird with 2 legs'); is($animals[0]->name, 'woody', ' It was the right animal'); @animals = eval { URT::Animal->get(num_legs => 0) }; like($@, qr/Unknown parameters to URT::Animal get()/, 'Correctly got an exception trying to query URT::Animal by num_legs'); 79_like_operator.t100664023532023421 307312544604517 16364 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 9; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my $thing = URT::Thing->get('value like' => '%One'); ok($thing, "Loaded thing iwth 'value like' => '%One'"); is($thing->id, 1, 'It was the right thing'); my @things = URT::Thing->get('value not like' => '%Two'); is(scalar(@things), 4, "Loaded 4 things with 'value not like' => '%Two'"); @things = URT::Thing->get('value like' => 'Number%'); is(scalar(@things), 5, "Got 5 things with 'value like' => 'Number%'"); @things = URT::Thing->get('value not like' => '%blah%'); is(scalar(@things), 5, "Got 5 things with 'value not like' => '%blah%'"); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value varchar)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); $sth->execute(1,'Number One'); $sth->execute(2,'Number Two'); $sth->execute(3,'Number Three'); $sth->execute(4,'Number Four'); $sth->execute(5,'Number Five'); $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 80_command_define_datasource.t100664023532023421 2562212544604517 20703 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use IO::File; use Test::More; my ($oracle,$postgres,$mysql); BEGIN { eval "use DBD::mysql"; eval "use DBD::Pg"; eval "use DBD::Oracle"; $oracle = $INC{"DBD/Oracle.pm"}; $mysql = $INC{"DBD/mysql.pm"}; $postgres = $INC{"DBD/Pg.pm"}; my $tests = 33; $tests += 12 if $oracle; $tests += 12 if $postgres; $tests += 12 if $mysql; plan tests => $tests; } BEGIN { use_ok('UR::Namespace::Command::Define::Datasource'); use_ok('UR::Namespace::Command::Define::Datasource::Sqlite'); use_ok('UR::Namespace::Command::Define::Datasource::Oracle'); use_ok('UR::Namespace::Command::Define::Datasource::Mysql'); use_ok('UR::Namespace::Command::Define::Datasource::Pg'); } my $data_source_dir = URT->get_base_directory_name() . '/DataSource/'; my @FILES_TO_DELETE = map { $data_source_dir . $_ } qw( TestcaseSqlite.pm TestcaseSqlite.sqlite3 TestcaseSqlite2.pm TestcaseOracle.pm TestcaseMysql.pm TestcasePg.pm ); push @FILES_TO_DELETE, '/tmp/TestcaseSqlite.sqlite3'; chdir $data_source_dir; my $cleanup_files = sub { unlink @FILES_TO_DELETE }; &$cleanup_files; UR::Namespace::Command::Define::Datasource->dump_status_messages(0); # don't print to the terminal # SQLite { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); ok($command->execute(),'Executed SQLite define'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite.sqlite3'; ok(-f $expected_path, 'Created SQLite database file'); $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite.pm'; ok(-f $expected_path, 'Created SQLite DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseSqlite/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseSqlite/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::SQLite/, "'is' line looks ok"); like($src, qr/sub server \{ \S+\/URT\/DataSource\/TestcaseSqlite.sqlite3/, 'server line looks ok'); unlike($src, qr/sub owner/, 'No owner line, as expected'); unlike($src, qr/sub login/, 'No login line, as expected'); unlike($src, qr/sub auth/, 'No auth line, as expected'); &$cleanup_files; } { my $db_file = '/tmp/TestcaseSqlite.sqlite3'; IO::File->new($db_file, 'w')->close(); my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite2 --server /tmp/TestcaseSqlite.sqlite3 ) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); ok($command->execute(),'Executed SQLite define'); my $expected_path = '/tmp/TestcaseSqlite.sqlite3'; ok(-f $expected_path, 'Created SQLite database file'); $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite2.pm'; ok(-f $expected_path, 'Created SQLite DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseSqlite/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseSqlite/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::SQLite/, "'is' line looks ok"); like($src, qr/sub server \{ '\/tmp\/TestcaseSqlite.sqlite3/, 'server line looks ok'); unlike($src, qr/sub owner/, 'No owner line, as expected'); unlike($src, qr/sub login/, 'No login line, as expected'); unlike($src, qr/sub auth/, 'No auth line, as expected'); # Don't remove the files because we want to test failure next } { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); $command->dump_error_messages(0); ok(! $command->execute(), 'Execute correctly returned failure'); my $message = $command->error_message; is($message,'A data source named URT::DataSource::TestcaseSqlite already exists', 'Error message mentions the target datasource module already exists'); &$cleanup_files; } # Oracle if($oracle) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(oracle --dsname TestcaseOracle --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Oracle DS'); open my $old_stderr, ">&STDERR"; close(STDERR); $command->dump_error_messages(0); # The execute() here will fail because TestcaseOracle isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Oracle define failed as expected'); like($@, qr/Failed to connect to the database/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseOracle.pm'; ok(-f $expected_path, 'Created Oracle DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseOracle/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseOracle/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::Oracle/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcaseOracle' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping Oracle tests since DBD::Oracle is not installed and configured"; } # PostgreSQL if ($postgres) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(pg --dsname TestcasePg --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Pg DS'); $command->dump_error_messages(0); open my $old_stderr, ">&STDERR"; close(STDERR); # The execute() here will fail because TestcasePg isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Pg define failed as expected'); like($@, qr/(Failed to connect to the database)|(Can't load \S+ for module DBD::Pg)/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcasePg.pm'; ok(-f $expected_path, 'Created Pg DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcasePg/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcasePg/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::Pg/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcasePg' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping PostgreSQL tests since DBD::pg is not installed"; } # MySQL if($mysql) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(mysql --dsname TestcaseMysql --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Mysql DS'); $command->dump_error_messages(0); open my $old_stderr, ">&STDERR"; close(STDERR); # The execute() here will fail because TestcaseMysql isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Mysql define failed as expected'); like($@, qr/Failed to connect to the database/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseMysql.pm'; ok(-f $expected_path, 'Created Mysql DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseMysql/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseMysql/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::MySQL/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcaseMysql' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping MySQL tests since DBD::mysql is not installed"; } sub _read_file { my $path = shift; my $fh = IO::File->new($path); die "Can't open $path: $!" unless $fh; # Read in the whole file local $/; undef $/; my $src = <$fh>; $fh->close(); return $src; } 80b_namespace_command_base.t100664023532023421 1316512544604517 20326 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 25; use Cwd; use File::Temp; my $test_directory = Cwd::abs_path(File::Basename::dirname(__FILE__)); my $original_cwd = Cwd::getcwd(); my $temp_dir = File::Temp::tempdir(CLEANUP => 1); ok(UR::Object::Type->define( class_name => 'URT::Command::TestBase', is => 'UR::Namespace::Command::Base', ), 'Define test command class'); URT::Command::TestBase->dump_error_messages(0); URT::Command::TestBase->queue_error_messages(1); chdir ($temp_dir) || die "Can't chdir to $temp_dir: $!"; my $namespace_name = URT::Command::TestBase->resolve_namespace_name_from_cwd(); ok(!defined($namespace_name), 'resolve_namespace_name_from_cwd returns nothing when not in a namespace directory'); my $cmd = URT::Command::TestBase->create(); ok(!$cmd, 'Cannot create command when pwd is not inside a namespace dir'); my $error_message = join("\n",URT::Command::TestBase->error_messages()); like($error_message, qr(Could not determine namespace name), 'Error message was correct'); my $lib_path = URT::Command::TestBase->resolve_lib_path_for_namespace_name('URT'); my($expected_lib_path) = ($test_directory =~ m/^(.*)\/URT\/t/); is($lib_path, $expected_lib_path, 'resolve_lib_path_for_namespace_name found the URT namespace'); $cmd = URT::Command::TestBase->create(namespace_name => 'URT'); ok($cmd, 'Created command in a temp dir with forced namespace_name'); is($cmd->namespace_name, 'URT', 'namespace_name is correct'); $expected_lib_path = $INC{'URT.pm'}; $expected_lib_path =~ s/\/URT.pm$//; is($cmd->lib_path, $expected_lib_path, 'lib_path is correct'); chdir($test_directory) || die "Can't chdir to $test_directory: $!"; $cmd = URT::Command::TestBase->create(); ok($cmd, 'Created command in the URT test dir and did not force namespace_name'); $lib_path = $cmd->lib_path; is($lib_path, $expected_lib_path, 'lib_path is correct'); chdir($lib_path) || die "Can't chdir to $lib_path: $!"; is($cmd->working_subdir, '.', 'when pwd is lib_path, working_subdir is correct'); chdir($test_directory) || die "Can't chdir to $test_directory"; is($cmd->working_subdir, 'URT/t', 'When pwd is the test directory, working_subdir is correct'); chdir($temp_dir) || die "Can't chdir to $temp_dir: $!"; my $expected_working_subdir = $lib_path . ('../' x scalar(my @l = split('/', Cwd::abs_path($lib_path)))) . $temp_dir; #is($cmd->working_subdir, $expected_working_subdir, 'when pwd is somwehere in /tmp, working_subdir is correct'); chdir($original_cwd); my $expected_namespace_path = $INC{'URT.pm'}; $expected_namespace_path =~ s/\.pm$//; is($cmd->namespace_path, $expected_namespace_path, 'namespace_path is correct'); is($cmd->command_name, 'u-r-t test-base', 'command_name is correct'); # This needs to be updated if we ever drop in a new module under URT/ my @expected_modules = sort qw(URT/34Baseclass.pm URT/38Primary.pm URT/43Primary.pm URT/FakeDBI.pm URT/ObjWithHash.pm URT/Thingy.pm URT/34Subclass.pm URT/38Related.pm URT/43Related.pm URT/RAMThingy.pm URT/Vocabulary.pm URT/Context/Testing.pm URT/DataSource/CircFk.pm URT/DataSource/Meta.pm URT/DataSource/SomeFile.pm URT/DataSource/SomeFileMux.pm URT/DataSource/SomeMySQL.pm URT/DataSource/SomeOracle.pm URT/DataSource/SomePostgreSQL.pm URT/DataSource/SomeSQLite.pm); my @modules = sort $cmd->_modules_in_tree(); # remove modules created by the 'ur update classes-from-db' test that may be running in parallel @modules = grep { $_ !~ m/Car.pm|Person.pm|Employee.pm/ } @modules; is_deeply(\@modules, \@expected_modules, '_modules_in_tree with no args is correct'); my @expected_class_names = sort qw(URT::34Baseclass URT::38Primary URT::43Primary URT::FakeDBI URT::ObjWithHash URT::Thingy URT::34Subclass URT::38Related URT::43Related URT::RAMThingy URT::Vocabulary URT::Context::Testing URT::DataSource::CircFk URT::DataSource::Meta URT::DataSource::SomeFile URT::DataSource::SomeFileMux URT::DataSource::SomeMySQL URT::DataSource::SomeOracle URT::DataSource::SomePostgreSQL URT::DataSource::SomeSQLite); my @class_names = sort $cmd->_class_names_in_tree; # remove classes created by the 'ur update classes-from-db' test that may be running in parallel @class_names = grep { $_ !~ m/URT::Car|URT::Person|URT::Employee/ } @class_names; is_deeply(\@class_names, \@expected_class_names, '_class_names_in_tree with no args is correct'); @modules = sort $cmd->_modules_in_tree( qw( URT/34Baseclass.pm URT/DataSource/Meta.pm URT/Something/NonExistent.pm URT::34Baseclass URT::DataSource::SomeOracle URT::NotAModule ) ); @expected_modules = sort qw( URT/34Baseclass.pm URT/DataSource/Meta.pm URT/34Baseclass.pm URT/DataSource/SomeOracle.pm ); is_deeply(\@modules, \@expected_modules, '_modules_in_tree with args is correct'); eval { my @tests = ( [q(Foo::Bar) , 1], [q(Foo'Bar) , 1], [q(Foo_Bar) , 1], [q(FooBar) , 1], [q(Foo0::Bar), 1], [q(Foo::0Bar), 1], [q(Foo.d) , ''], [q(0Foo::Bar), ''], ); for my $test (@tests) { my ($class_name, $is_valid) = @$test; my $msg = $is_valid ? "valid: $class_name" : "invalid: $class_name"; is(UR::Util::is_valid_class_name($class_name), $is_valid, $msg); } }; 80c_command_describe.t100664023532023421 277312544604517 17144 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 3; my $cmd = UR::Namespace::Command::Show::Properties->create(classes_or_modules => ['URT::Thingy'], namespace_name => 'URT'); ok($cmd, 'Create UR::Namespace::Command::Show::Properties'); my $output = ''; close STDOUT; open(STDOUT, '>', \$output) || die "Can't open STDOUT: $!"; ok($cmd->execute(), 'Execute()'); my $expected_output = < 19; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Create database'); $dbh->do('create table workplace (workplace_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL)'); $dbh->do("insert into workplace values (1, 'Acme')"); $dbh->do("insert into workplace values (2, 'CoolCo')"); $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, workplace_id integer REFERENCES workplace(workplace_id))'); $dbh->do("insert into person values (1, 'Bob',1)"); $dbh->do("insert into person values (2, 'Fred',2)"); $dbh->do("insert into person values (3, 'Mike',1)"); $dbh->do("insert into person values (4, 'Joe',2)"); UR::Object::Type->define( class_name => 'URT::Workplace', id_by => 'workplace_id', has => [ name => { is => 'String' }, uc_name => { calculate_from => ['name'], calculate => q( return uc $name ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'workplace', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, uc_name => { calculate_from => ['name'], calculate => q( return uc $name ) }, workplace => { is => 'URT::Workplace', id_by => 'workplace_id' }, workplace_name => { via => 'workplace', to => 'name' }, workplace_uc_name => { via => 'workplace', to => 'uc_name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); my $counter = 0; sub URT::Person::a_sub { return $counter++ } my @p = URT::Person->__meta__->properties; my($fh,$output); $output = ''; open($fh, '>', \$output); # Query involving only one class, filter is a direct property, show has a calculated property my $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Workplace', filter => 'name=CoolCo', show => 'id,uc_name', output => $fh); ok($cmd, 'Create a lister command for Workplace. filter has direct, show has calculated'); ok($cmd->execute(), 'execute'); my $expected_output = <', \$output); # filter is a calculated property, show has both calculated and direct properties $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Workplace', filter => 'uc_name=COOLCO', show => 'id,uc_name,name', output => $fh); ok($cmd, 'Create a lister command for Workplace. filter has calculated, show has direct and calculated'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving two joined tables, filter is a via/to property, show has calculated and via/to properties $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'workplace_name=Acme', show => 'uc_name,workplace_uc_name', output => $fh); ok($cmd, 'Create a lister command for Person. filter has via/to, show has calculated and via/to'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving two joined tables, filter is a direct property, show has direct and via/to property $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'name~%o%', show => 'name,workplace_name', output => $fh); ok($cmd, 'Create a lister command for Person. filter has direct prop, show has direct and via/to'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving one table and calling a subroutine directly $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'name~%o%', show => 'name,$o->a_sub', output => $fh); ok($cmd, 'Create a lister command for Person with a subroutine in the show list'); ok($cmd->execute(), 'execute'); $expected_output = <<'EOS'; NAME ($O->A_SUB) ---- ----------- Bob 0 Joe 1 EOS is($output, $expected_output, 'Output is as expected'); $output = ''; open($fh, '>', \$output); $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', show => 'id,name,workplace_name', order_by => 'workplace_name', output => $fh); ok($cmd, 'Create a lister command for Person with a custom order-by'); ok($cmd->execute(), 'execute'); $expected_output = <<'EOS'; ID NAME WORKPLACE_NAME -- ---- -------------- 1 Bob Acme 3 Mike Acme 2 Fred CoolCo 4 Joe CoolCo EOS is($output, $expected_output, 'Output is as expected'); 81_crud_custom_columnnames.t100664023532023421 655312544604517 20454 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; &create_tables_and_classes(); my $p1 = URT::Product->get(1); ok(!$p1, 'Get by non-existent ID correctly returns nothing'); my $p2 = URT::Product->create(id => 1, name => 'jet pack', genius => 6, manufacturer_name => 'Lockheed Martin',cost => 5); ok($p2, 'Create a new Product with the same ID'); $p1 = URT::Product->get(1); ok($p1, 'Get with the same ID returns something, now'); is($p1->id, 1, 'ID is correct'); is($p1->name, 'jet pack', 'name is correct'); is($p1->genius, 6, 'name is correct'); is($p1->manufacturer_name, 'Lockheed Martin', 'name is correct'); my $p3 = URT::Product->get(100); ok($p3, 'Retrieve product with ID 100'); is($p3->cost, 100, 'Its cost is 100'); is($p3->genius, 1, 'Its genius is 1'); ok($p3->cost(5000), 'Change cost to 5000'); ok($p3->genius(99), 'Change genius to 99'); my $p4 = URT::Product->get(101); ok($p4, 'Retrieve product with ID 101'); ok($p4->delete, 'Delete it'); ok(UR::Context->commit(), 'Commit'); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; my $sth = $dbh->prepare('select * from product'); $sth->execute(); my %products_by_id; while (my $row = $sth->fetchrow_hashref) { my %copy = %$row; $products_by_id{$copy{'product_prod_id'}} = \%copy; } $sth->finish; is(scalar(keys %products_by_id), 2, 'There were 2 products in the database'); my $expected = { 1 => { product_prod_id => 1, product_name => 'jet pack', product_genius => 6, product_mfg_name => 'Lockheed Martin', cost => 5, }, 100 => { product_prod_id => 100, product_name => 'Something to update', product_genius => 99, product_mfg_name => 'Acme', cost => 5000, }, }; is_deeply(\%products_by_id, $expected, 'Data in DB is as expected'); #note(Data::Dumper::Dumper(\%products_by_id)); sub create_tables_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PRODUCT ( product_prod_id int NOT NULL PRIMARY KEY, product_name varchar, product_genius integer, product_mfg_name varchar, cost integer)'), 'created product table'); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', id_by => [ prod_id => { is => 'NUMBER', sql => 'product_prod_id' }, ], has => [ name => { is => 'STRING', sql => 'product_name' }, genius => { is => 'NUMBER', sql => 'product_genius' }, manufacturer_name => { is => 'STRING', sql => 'product_mfg_name' }, cost => { is => 'NUMBER' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); ok($dbh->do("insert into product (product_prod_id,product_name,product_genius,product_mfg_name,cost) values (100,'Something to update',1,'Acme',100)"), 'Inserted item 1'); ok($dbh->do("insert into product (product_prod_id,product_name,product_genius,product_mfg_name,cost) values (101,'Something to delete',1,'Acme',200)"), 'Inserted item 101'); $dbh->commit(); } 82_boolexpr_op_underscore.t100775023532023421 66512544604517 20267 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use UR; class Foo { has => [ _bar => {}, ], }; my $bx1 = Foo->define_boolexpr( _bar => { operator => '!=', value => undef}); my $bx2 = Foo->define_boolexpr( '_bar !=' => undef); is( $bx2->id, $bx1->id, "Boolean expression created with an operator, with an operator using the new syntax and using a parameter name with an underbar works."); 82a_boolexpr_op_case_insensitive.t100664023532023421 200412544604517 21614 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use UR; class Foo { has => [ _bar => {}, _baz => {}, ], }; sub evaluate_permutations_for_boolexps_with_message { my $bx1 = shift; my $bx2 = shift; my $msg = shift; for my $obj (Foo->create( _bar => 0, _baz => 0 ), Foo->create( _bar => 1, _baz => 0 ), Foo->create( _bar => 1, _baz => 1 ), Foo->create( _bar => 0, _baz => 1 )){ is( $bx1->evaluate($obj), $bx2->evaluate($obj), $msg ); } } my $bx1 = Foo->define_boolexpr('_bar != 1 and _baz != 1'); my $bx2 = Foo->define_boolexpr('_bar != 1 AND _baz != 1'); my $bx3 = Foo->define_boolexpr('_bar != 1 or _baz != 1'); my $bx4 = Foo->define_boolexpr('_bar != 1 OR _baz != 1'); evaluate_permutations_for_boolexps_with_message($bx1, $bx2, "Lower and uppercase AND behave the same"); evaluate_permutations_for_boolexps_with_message($bx3, $bx4, "Lower and uppercase OR behave the same"); done_testing(); 83_commit_between_schemas.t100664023532023421 1156212544604517 20246 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 17; use DBD::SQLite; print $DBD::SQLite::VERSION,"\n"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; use File::Temp; use URT::DataSource::SomeSQLite; # This tests a case where there are two tables, one in the default schema and one in # an attached schema, and there is a foreign key between the two tables requiring # UR::DataSource::RDBMS::_sync_database to process it as a prerequsite. There was a bug # that could cause data to get dropped on the floor in this case (fixed in commit da174c) our($tmp_file1, $tmp_file2); $tmp_file1 = File::Temp::tmpnam() . "_ur_testsuite_83_db1.sqlite3"; $tmp_file2 = File::Temp::tmpnam() . "_ur_testsuite_83_db2.sqlite3"; END { unlink $tmp_file1 if defined $tmp_file1; unlink $tmp_file2 if defined $tmp_file2; } my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); # A sqlite-ism way of pretending we have different schemas my $autocommit = $dbh->{'AutoCommit'}; $dbh->{'AutoCommit'} = 1; # I'd rather use memory DBs, but SQLite segfaults in commit() below ok($dbh->do("attach database '$tmp_file1' as PROD_DB"), 'defined PROD_DB schema'); ok($dbh->do("attach database '$tmp_file2' as PEOPLE"), 'defined PEOPLE schema'); $dbh->{'AutoCommit'} = $autocommit; ok($dbh->do('create table PEOPLE.PEOPLE ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created product table'); ok($dbh->do('create table PROD_DB.PRODUCT ( product_prod_id int NOT NULL PRIMARY KEY, product_name varchar, creator_id integer references PEOPLE(person_id))'), 'created product table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PEOPLE.PEOPLE', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for product creator'); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', id_by => [ prod_id => { is => 'NUMBER', sql => 'product_prod_id' }, ], has => [ name => { is => 'STRING', sql => 'product_name' }, creator => { is => 'URT::Person', id_by => 'creator_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); $dbh->commit(); # SQLite doesn't really do foreign key constraints, and really doesn't do them # between databases, so insert some metaDB info about a foreign key between # the product's creator and a person's ID sub URT::DataSource::SomeSQLite::owner { 'PROD_DB'; } # the default schema/owner sub URT::DataSource::SomeSQLite::get_foreign_key_details_from_data_dictionary { my $self = shift; my($fk_catalog,$fk_schema,$fk_table,$pk_catalog,$pk_schema,$pk_table) = @_; unless ($fk_table eq 'PRODUCT' or $pk_table eq 'PEOPLE') { return UR::DataSource::SQLite::get_foreign_key_details_from_data_dictionary($self,@_); } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( FK_NAME UK_TABLE_NAME UK_COLUMN_NAME UK_TABLE_SCHEM FK_TABLE_NAME FK_COLUMN_NAME FK_TABLE_SCHEM ); my $table = $pk_table || $fk_table; my @ret_data = ( { FK_NAME => 'product_person_fk', UK_TABLE_SCHEM => 'PEOPLE', UK_TABLE_NAME => 'PEOPLE', UK_COLUMN_NAME => 'person_id', FK_TABLE_SCHEM => 'PROD_DB', FK_TABLE_NAME => 'PRODUCT', FK_COLUMN_NAME => 'creator_id' } ); my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @ret_data ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } my $person = URT::Person->create(person_id => 1, name => 'Bob'); ok($person, 'Created a person'); my $product = URT::Product->create(prod_id => 1, name => 'Jet Pack', creator => $person); ok($product, 'Created a product created by that person'); ok(UR::Context->commit, 'Commit'); my $data = $dbh->selectrow_hashref('select * from PROD_DB.PRODUCT where product_prod_id = 1'); ok($data, 'Got back data from the DB for the product'); is($data->{'product_prod_id'}, 1, 'product_id ok'); is($data->{'product_name'}, 'Jet Pack', 'name ok'); is($data->{'creator_id'}, 1, 'creator_id ok'); $data = $dbh->selectrow_hashref('select * from PEOPLE.PEOPLE where person_id = 1'); ok($data, 'Got back data from the DB for the creator'); is($data->{'person_id'}, 1, 'person_id ok'); is($data->{'name'}, 'Bob', 'name ok'); 84_class_definition_errors.t100664023532023421 236112544604517 20431 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', is => 'NonExistentClass', has => 'property_a', ); }; ok(! $o, 'Defining class with non-existant parent did not work'); like($@, qr/cannot initialize because of errors using parent class NonExistentClass/, 'Error message looks correct'); $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', is => 'URT::NonExistentClass', has => 'property_a', ) }; ok(! $o, 'Defining class with non-existant parent did not work'); like($@, qr/cannot initialize because of errors using parent class URT::NonExistentClass/, 'Error message looks correct'); $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', has => [ 'prop' => { is => 'URT::NonExistantClass', id_by => 'prop_id' }, ], ) }; ok(! $o, 'Defining class with relationship to non-existant class did not work'); like($@, qr/Unable to load URT::NonExistantClass while defining relationship prop/, 'Error message looks correct'); 84b_implied_properties.t100664023532023421 354112544604517 17562 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use above 'UR'; use Test::More tests => 9; UR::Object::Type->define(class_name => 'Sandwich'); UR::Object::Type->define(class_name => 'Drink'); UR::Object::Type->define( class_name => 'Combo', id_by => [ sandwich => { is => 'Sandwich' }, drink => { is => 'Drink' }, ], ); UR::Object::Type->define( class_name => 'Order', has => [ sandwich => { is => 'Sandwich', id_by => 'sandwich_id' }, drink => { is => 'Drink' }, ], ); UR::Object::Type->define( class_name => 'BuggedOrder', has => [ # sandwich has to have the id_by here in order to trigger the bug sandwich => { is => 'Sandwich', id_by => 'sandwich_id' }, drink => { is => 'Drink' }, # yes, drink_id is ommitted here ], has_optional => [ combo => { is => 'Combo', id_by => ['sandwich_id', 'drink_id'], # This drink_id is not related to 'drink' above }, ], ); my $sandwich = Sandwich->create; isa_ok($sandwich, 'Sandwich', 'sandwich'); my $drink = Drink->create; isa_ok($drink, 'Drink', 'drink'); my $ok_order = Order->create(sandwich => $sandwich, drink => $drink); isa_ok($ok_order, 'Order', 'ok_order'); is($ok_order->__meta__->property('sandwich')->is_optional, 0, 'sandwich is not optional'); my $order = BuggedOrder->create(sandwich => $sandwich, drink => $drink); isa_ok($order, 'BuggedOrder', 'order'); my $order_meta = $order->__meta__; is($order_meta->property('sandwich_id')->is_optional, 0, 'sandwich_id is not optional'); is($order_meta->property('sandwich')->is_optional, 0, 'sandwich is not optional'); is($order_meta->property('drink')->is_optional, 0, 'drink is not optional'); # because drink_id isn't mentioned in the definition of drink, but is for combo is($order_meta->property('drink_id')->is_optional, 1, 'drink_id is optional'); 85_avoid_loading_using_hints.t100664023532023421 1166612544604517 20762 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 18; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color' }, car_parts => { via => 'cars', to => 'parts', is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my @people = URT::Person->get(is_cool => 1, -hints => ['car_parts']); is(scalar(@people), 3, '3 people are cool'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @car = $people[0]->cars; is(scalar(@car), 2, 'Got car objects from first person through accessor'); is($query_count, 0, 'Made no queries'); $query_count = 0; @car = URT::Car->get(owner_id => $people[0]->id); is(scalar(@car),2 , 'Got car objects from first person from URT::Car class'); is($query_count, 0, 'Made no queries'); $query_count = 0; @people = URT::Person->get(is_cool => 1); is(scalar(@people), 3, '3 people are cool (no hints)'); is($query_count, 0, 'Made no queries'); $query_count = 0; my @parts = $people[0]->primary_car->parts; is(scalar(@parts), 2, "First person's car has 2 parts"); is($query_count, 0, 'Made no queries'); 85_method_meta.t100664023532023421 44412544604517 15767 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More skip_all => 'under development'; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; package Foo; class Foo { }; package main; isa_ok('Foo',"UR::Object"); 85b_avoid_loading_using_hints.t100664023532023421 650012544604517 21073 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # This tests a bugfix where specifying a hints to a property that # includes a where-clause would omit the where params in the template/rule # that gets recorded in the query cache. As a result, a later query could # incorrectly think it had already been loaded and miss data. use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1, }, primary_car => { is => 'URT::Car', reverse_as => 'owner', where => ['is_primary true' => 1], is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my $person = URT::Person->get(name => 'Bob', -hints => ['primary_car_color']); ok($person, 'Got a person named Bob'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my $color = $person->primary_car_color(); is($color, 'yellow', "Bob's primary car color is yellow"); is($query_count, 0, 'Made no queries'); $query_count = 0; my @cars = URT::Car->get(owner_id => $person->id); is(scalar(@cars), 2, 'Bob has 2 cars'); is($query_count, 1, 'Made 1 query'); 86_custom_load.t100664023532023421 300012544604517 16022 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; package Foo; class Foo { id_by => ['a'], #has => ['b','c', 'd' => { calculate_from => ['b','c'], calculate => q|$b+$c| }] has => [qw/a b c/], }; sub __load__ { return ['id', 'a', 'b', 'c'], [ ['a1', 'a1', 'b1', 'c1'], ['a2', 'a2', 'b2', 'c2'], ['a3', 'a3', 'b3', 'c3'], ] }; package main; use Test::More tests=> 16; my $o1 = Foo->get('a2'); ok($o1, "got object 2 back"); is($o1->id, 'a2', 'id is correct'); is($o1->a, 'a2', 'property a is correct'); is($o1->b, 'b2', 'property b is correct'); is($o1->c, 'c2', 'property c is correct'); my @o = Foo->get(); is(scalar(@o), 3, "got objects back"); package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; my $data_set_size = 100_000; sub __load__ { my $props = ['id','a','b','c']; my $data = IO::File->new("yes abcdefg| head -n $data_set_size |"); my $n = 0; my $iterator = sub { my $v = $data->getline; if (not defined $v) { $data->close(); return; } chomp $v; $n++; return [$n,$n,$v,$v]; }; return ($props, $iterator); } package main; my $i = Bar->create_iterator(); my $n = 0; while (my $o = $i->next()) { $n++; if ($n % 10_000 == 0) { ok(1,"processed $n"); } } 86b-custom-load-join.t100664023532023421 254712544604517 16774 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use UR; use Test::More tests => 8; note("*** class 1: like-clause ***"); class Acme::Foo { has => [qw/a b c/] }; sub Acme::Foo::__load__ { return [qw/id a b c/], [ [100, "a100", "b100", "c100"], [200, "a200", "b200", "c200"], [300, "a300", "b300", "c300"], ] } my @f = Acme::Foo->get("b like" => "%2%"); is(scalar(@f), 1, "got one object with a like-clause"); is($f[0]->id, 200, "it is correct"); note("*** class 2: in-clause ***"); class Acme::Bar { has => [ a => { is => 'Text' }, b => { is => 'Text' }, c => { is => 'Text' }, foo => { is => "Acme::Foo", id_by => "foo_id" }, ] }; sub Acme::Bar::__load__ { return [qw/id a b c foo_id/], [ [10, "a100", "b100", "c100", 100], [20, "a200", "b200", "c200", 200], [30, "a300", "b300", "c300", 300], ] } my @b = Acme::Bar->get("c" => ['c200', 'c300']); is(scalar(@b), 2, "got two objects with an in-clause"); is($b[0]->id, 20, "first is correct"); is($b[1]->id, 30, "second is correct"); note("*** in-memory joins ***"); my @b2 = Acme::Bar->get("foo.a" => "a100"); is(scalar(@b2), 1, "got one object with a join to another class"); is($b2[0]->id, 10, "it is the correct object"); is($b2[0]->foo->a, "a100", "value is correct"); 87_attributes_have.t100664023532023421 232512544604517 16714 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; use Test::More tests => 14; class DoIt { is => 'Command', has => { i => { is_input => 1 }, o => { is_output => 1 }, p => { is_param => 1 }, } }; my $m = DoIt->__meta__; ok($m, 'got meta object for the class'); my $pi = $m->property('i'); ok($pi, 'got meta property for attribute i'); ok($pi->{is_input}, "flag is set for input"); ok(!$pi->{is_output}, "flag is not set for output"); ok(!$pi->{is_param}, "flag is not set for param"); ok($pi->is_input(), "is_input returns true"); ok(!$pi->is_output(), "is_output returns false"); ok(!$pi->is_param(), "is_output returns false"); eval { $pi->foo }; ok($@, "calling odd methods fails"); class SomeThing { has => 'x' }; my $m2 = SomeThing->__meta__; ok($m2, "got property meta for regular class"); my $px = $m2->property('x'); ok($px, 'got meta property for attribute x'); ok(!$px->{is_input}, "flag is not set for input"); eval { $px->is_input() }; ok($@, "is_input accessor attempt throws exception"); eval { $px->foo }; ok($@, "calling odd methods fails"); 87_get_by_different_params_updates_query_cache.t100664023532023421 422712544604517 24465 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; use URT::DataSource::SomeSQLite; # Get an object into memory with a query. Re-get it with a second query (which will hit the DB # again because it doesn't know it doesn't really have to). Finally, do the second query again # and it should not hit the DB my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); # Insert some data # Bob, Joe and Frank are cool # Fred and Mike are not my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(name => ['Bob','Joe','Frank']); is(scalar(@p), 3, 'Got 3 people with an in-clause'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @p = URT::Person->get(is_cool => 1); is(scalar(@p), 3, 'Got the same 3 people with a different query'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @p = URT::Person->get(is_cool => 1); is(scalar(@p), 3, 'Got the same 3 people with the second query again'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 0, 'Made 1 query'); 87_is_many_indirect_is_efficient.t100664023532023421 1342112544604517 21571 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 15; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Tests that calling car_parts_prices on a URT::Person object is efficient # The AccessorWriter does the retrieval differently if these conditions hold: # 1) car_parts_prices is_delegated and is_many # 2) the thing it is 'via' (cars) is an object accessor (has a data_type: URT::Car) and is_many # 3) the thing it is 'to' (parts_prices) is_delegated and has a via as well # 4) parts_prices is via something that is an object accessor with a data_type (URT::CarParts) # # If these hold, then it can do the query differently: # 1) Call URT::Car->get() with appropriate params that $person->cars would use # 2) For each of the objects resulting from #1 (URT::Car), extract out the value that links # these to the final class (URT::CarParts) # 3) Do a get on the final class filtering on the linking property and an 'in' clause # with the values from #2 use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color' }, car_parts => { is => 'URT::CarParts', via => 'cars', to => 'parts', is_optional => 1, is_many => 1 }, car_parts_prices => { via => 'cars', to => 'parts_prices', is_optional => 1, is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, parts_prices => { via => 'parts', to => 'price', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @colors = $person->cars(); is(scalar(@colors), 2, 'person has 2 cars with colors'); is($query_count, 1, 'made 1 query'); $query_count = 0; my @prices = $person->car_parts_prices(); is(scalar(@prices), 4, "person's cars have 4 car_parts with prices"); is($query_count, 1, 'Made 1 query'); URT::CarParts->unload(); $query_count = 0; my @parts = $person->car_parts; is($query_count, 1, 'Made 1 query getting car_parts for person'); my @parts_ids = sort { $a <=> $b } map { $_->id } @parts; is_deeply(\@parts_ids, [1, 2, 9, 10], 'Got the correct CarParts objects'); 87a_many_to_many_query_is_efficient.t100664023532023421 754412544604517 22322 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__).'/../../../lib'; use lib File::Basename::dirname(__FILE__).'/../..'; # Tests that for two entities with bridge objects connecting them one can # efficiently retrieve all of the associated entities across the bridge use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table CLUB ( club_id int NOT NULL PRIMARY KEY, name varchar )'), 'created club table'); ok($dbh->do('create table MEMBERSHIP ( membership_id int NOT NULL PRIMARY KEY, person_id int references PERSON(person_id), club_id int references CLUB(club_id))'), 'created membership table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, memberships => { is => 'URT::Membership', is_many => 1, reverse_as => 'member' }, clubs => { is => 'URT::Club', is_many => 1, via => 'memberships', to => 'club' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Club', table_name => 'CLUB', id_by => [ club_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, memberships => { is => 'URT::Membership', is_many => 1, reverse_as => 'club' }, members => { is => 'URT::Person', is_many => 1, via => 'memberships', to => 'member' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for clubs'); ok(UR::Object::Type->define( class_name => 'URT::Membership', table_name => 'MEMBERSHIP', id_by => [ membership_id => { is => 'NUMBER' }, ], has => [ person_id => { is => 'NUMBER' }, member => { is => 'URT::Person', id_by => 'person_id' }, club_id => { is => 'CLUB' }, club => { is => 'URT::Club', id_by => 'club_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); #insert data #Alice, Bob, and Charlie are members of Club A #Alice, Charlie, and Darlene are members of Club B #Alice and Charlie are members of Club C #Alice is a member of Club D my $insert = $dbh->prepare('insert into person values (?,?)'); for my $row ([1, 'Alice'], [2, 'Bob'], [3, 'Charlie'], [4, 'Darlene']) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into club values (?,?)'); for my $row ([100, 'Club A'], [200, 'Club B'], [300, 'Club C'], [400, 'Club D']) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into membership values (?,?,?)'); for my $row ([101, 1, 100], [102, 2, 100], [103, 3, 100], [201, 1, 200], [203, 3, 200], [204, 4, 200], [301, 1, 300], [303, 3, 300], [401, 1, 400], ){ $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_text = $_[0]; $query_count++} ), 'created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @clubs = $person->clubs(); is(scalar(@clubs), 4, 'got all 4 clubs of which person is a member'); is($query_count, '2', 'made 2 queries total'); #one to get memberships, one to get clubs my $club = URT::Club->get(200); ok($club, 'Got club object'); $query_count = 0; my @members = $club->members(); is(scalar(@members), 3, 'got all 3 members of the club'); is($query_count, '2', 'made 2 queries total'); #one to get memberships, one to get members 87b_is_many_id_class_by_is_efficient.t100664023532023421 1222312544604517 22404 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # This test tries getting a property delegated through an object accessor # with an id_class_by, effectively making it doubly-delegated # # In this situation, the accessor should collect the bridge objects # (Inventory in this test), bucket them by final result class, and # then do a single get() for each result class with the IDs of # the result items collected from the bridge objects use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar)'), 'created person table'); ok($dbh->do('create table INVENTORY ( inv_id int NOT NULL PRIMARY KEY, owner_id integer, value_id varchar, value_class varchar, category varchar)'), 'created inventory table'); ok($dbh->do('create table PROPERTY ( property_id int NOT NULL PRIMARY KEY, name varchar, size integer)'), 'created item table'); ok($dbh->do('create table ITEM ( item_id int NOT NULL PRIMARY KEY, name varchar, size integer)'), 'created item table'); UR::Object::Type->define( class_name => 'URT::OwnedThing', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Property', is => 'URT::OwnedThing', doc => 'Things someone can own that has a record of title', id_by => 'property_id', has => ['name','size'], table_name => 'PROPERTY', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Item', is => 'URT::OwnedThing', doc => 'Things someone can own that has no record of title', id_by => 'item_id', has => ['name','size'], table_name => 'ITEM', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, ], has_many => [ inventory => { is => 'URT::Inventory', reverse_as => 'owner' }, vehicles => { is => 'URT::Property', via => 'inventory', to => 'thing', where => [category => 'vehicles'] }, money => { is => 'URT::Item', via => 'inventory', to => 'thing', where => [category => 'money'] }, things => { is => 'URT::OwnedItem', via => 'inventory', to => 'thing' }, ], table_name => 'PERSON', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Inventory', id_by => 'inv_id', has => [ category => { is => 'String' }, thing => { is => 'URT::OwnedThing', id_by => 'value_id', id_class_by => 'value_class' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], table_name => 'INVENTORY', data_source => 'URT::DataSource::SomeSQLite', ); # Insert some data # Bob has 2 cars, a house, 3 pieces of money and a dog # Fred has 1 car, 1 snowmobile and a cat my $insert = $dbh->prepare('insert into person values (?,?)'); foreach my $row ( [ 1, 'Bob'], [2,'Fred'] ) { $insert->execute(@$row); } $insert->finish; $insert = $dbh->prepare('insert into item values (?,?,?)'); foreach my $row ( [ 1, 'coin', 1], [2, 'dollar', 2], [3, 'coin', 1], [4, 'dog', 10], [ 5, 'cat', 8], ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into property values (?,?,?)'); foreach my $row ( [ 1, 'blue car', 100], [2, 'house', 1000], [3, 'red car', 200], [ 4, 'yellow car', 100], [5, 'snowmobile', 50], ) { $insert->execute(@$row); } $insert->finish(); # id, owner_id, value_id, value_class, category $insert = $dbh->prepare('insert into inventory values (?,?,?,?,?)'); foreach my $row ( [1, 1, 1, 'URT::Item', 'money'], [2, 1, 2, 'URT::Item', 'money'], [3, 1, 3, 'URT::Item', 'money'], [4, 1, 4, 'URT::Item', 'livestock'], [5, 1, 1, 'URT::Property', 'vehicles'], [6, 1, 2, 'URT::Property', 'land'], [7, 1, 3, 'URT::Property', 'vehicles'], [8, 2, 5, 'URT::Item', 'livestock'], [9, 2, 4, 'URT::Property', 'vehicles'], [10, 2, 5, 'URT::Property', 'vehicles'], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @money = $person->money(); is(scalar(@money), 3, 'person has 3 pieces of money'); is($query_count, 2, 'made 2 queries'); # 1 for the inventory bridges and 1 for all the money items $person = URT::Person->get(2); ok($person, 'Got a different person'); $query_count = 0; my @things = $person->things(); is(@things, 3, 'Second person has 3 things'); is($query_count, 3, 'Made 3 queries'); # 1 for the inventory bridges, 1 for the Items and 1 for the Propertys 87c_query_by_is_many_indirect_is_efficient.t100664023532023421 1270312544604517 23655 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 20; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color', is_many => 1 }, car_parts => { is => 'URT::CarParts', via => 'cars', to => 'parts', is_optional => 1, is_many => 1 }, car_parts_prices => { via => 'cars', to => 'parts_prices', is_optional => 1, is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, parts_prices => { via => 'parts', to => 'price', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1], [6, 'Hobo Cliff', 0] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my @people = URT::Person->get(car_color => 'pink'); is(scalar(@people), 0, 'No person has a pink car'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @people = URT::Person->get(car_color => 'red'); is(scalar(@people), 2, '2 people have red cars'); is($query_count, 1, 'Made 1 query'); is($people[0]->name, 'Bob', 'Bob is the first person returned'); is($people[1]->name, 'Mike', 'Mike is the second person returned'); $query_count = 0; my @cars = URT::Car->get(owner_id => $people[1]->id, color => 'red'); is(scalar(@cars), 1, 'Mike has 1 red car'); is($query_count, 0, 'Made no queries'); $query_count = 0; @cars = URT::Car->get(owner_id => $people[0]->id); is(scalar(@cars), 2, 'Bob has 2 cars'); is($query_count, 1, 'Made 1 query'); # Needed to query since the first via URT::Person only loaded red cars $query_count = 0; @people = URT::Person->get(is_cool => 0, -hints => ['cars']); is(scalar(@people), 3, "got three people, with a hint to get their cars, when one has no cars"); for my $person (@people) { my @cars = $person->cars(); note("person $person has " . scalar(@cars) . " cars"); } is($query_count, 1, 'Made 1 query. The hints loaded all the related cars'); 87d_query_by_is_many_indirect_is_efficient.t100664023532023421 1403312544604517 23654 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table MAINTABLE ( main_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table RELATED1 (related1_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES maintable(main_id), value varchar)'), 'created related1 table'); ok($dbh->do('create table RELATED2 (related2_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related1(related1_id), value varchar)'), 'created related2 table'); ok($dbh->do('create table RELATED3 (related3_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related2(related2_id), value varchar)'), 'created related3 table'); ok($dbh->do('create table RELATED4 (related4_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related3(related3_id), value varchar)'), 'created related4 table'); $dbh->do("insert into maintable values (1,'Bob')"); $dbh->do("insert into related1 values (1,1,'related1')"); $dbh->do("insert into related2 values (1,1,'related2')"); $dbh->do("insert into related3 values (1,1,'related3')"); $dbh->do("insert into related4 values (1,1,'related4')"); $dbh->do("insert into maintable values (2,'Joe')"); $dbh->do("insert into related1 values (2,2,'related1alt')"); $dbh->do("insert into related2 values (2,2,'related2alt')"); $dbh->do("insert into related3 values (2,2,'related3alt')"); $dbh->do("insert into related4 values (2,2,'related4alt')"); ok(UR::Object::Type->define( class_name => 'URT::Main', table_name => 'maintable', id_by => [ main_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, ], has_many => [ related_1s => { is => 'URT::Related1', reverse_as => 'related' }, related_values => { via => 'related_1s', to => 'value' }, related_2s => { is => 'URT::Related2', via => 'related_1s', to => 'related2s' }, related_2_values => { via => 'related_2s', to => 'value' }, related_3s => { is => 'URT::Related3', via => 'related_2s', to => 'related3s' }, related_3_values => { via => 'related_3s', to => 'value' }, related_4s => { is => 'URT::Related4', via => 'related_3s', to => 'related4s' }, related_4_values => { via => 'related_4s', to => 'value' }, related_4_values_alt => { via => 'related_1s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for main'); ok(UR::Object::Type->define( class_name => 'URT::Related1', table_name => 'related1', id_by => [ related1_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Main', id_by => 'related_id' }, value => { is => 'string' }, related2s => { is => 'URT::Related2', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related2s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 1"); ok(UR::Object::Type->define( class_name => 'URT::Related2', table_name => 'related2', id_by => [ related2_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related1', id_by => 'related_id' }, value => { is => 'string' }, related3s => { is => 'URT::Related3', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related3s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 2"); ok(UR::Object::Type->define( class_name => 'URT::Related3', table_name => 'related3', id_by => [ related3_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related2', id_by => 'related_id' }, value => { is => 'string' }, related4s => { is => 'URT::Related4', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related4s', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 3"); ok(UR::Object::Type->define( class_name => 'URT::Related4', table_name => 'related4', id_by => [ related4_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related3', id_by => 'related_id' }, value => { is => 'string' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 4"); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $thing; $query_count = 0; #$DB::single=1; $thing = URT::Main->get(related_4_values => 'related4'); ok($thing, 'Got one object for a 5-table join'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Related1->get(related_id => 1); ok($thing, 'Got 1 related URT::Related1 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related2->get(related_id => 1); ok($thing, 'Got 1 related URT::Related2 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related3->get(related_id => 1); ok($thing, 'Got 1 related URT::Related3 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related4->get(related_id => 1, value => 'related4'); ok($thing, 'Got 1 related URT::Related4 thing by related_id'); is($query_count, 0, 'Made no queries'); 87e_missing_hangoff_data_is_efficient.t100664023532023421 1702412544604517 22553 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 44; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # Tests a class that has optional hangoff data. # query for objects, including hints for the hangoffs, and then call the # accessor for the hangoff data. The accessors should not trigger additional # DB queries, even for those with missing hangoff data. my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table PERSON_INFO (pi_id int NOT NULL PRIMARY KEY, person_id integer REFERENCES person(person_id), key varchar, value_class_name varchar, value_id varchar)'), 'created person_info table'); $dbh->do("insert into person values (1,'Kermit')"); $dbh->do("insert into person_info values (1,1,'color', 'UR::Value::Text', 'green')"); $dbh->do("insert into person_info values (2,1,'species', 'UR::Value::Text','frog')"); $dbh->do("insert into person_info values (3,1,'food', 'UR::Value::Text','flies')"); $dbh->do("insert into person values (2,'Miss Piggy')"); $dbh->do("insert into person_info values (4,2,'color','UR::Value::Text','pink')"); $dbh->do("insert into person_info values (5,2,'species','UR::Value::Text','pig')"); $dbh->do("insert into person_info values (6,2,'sport','UR::Value::Text','karate')"); $dbh->do("insert into person_info values (7,2,'truelove','URT::Person','1')"); $dbh->do("insert into person values (3,'Fozzy')"); $dbh->do("insert into person_info values (8,3,'color','UR::Value::Text','brown')"); $dbh->do("insert into person_info values (9,3,'species','UR::Value::Text','bear')"); $dbh->do("insert into person_info values (10,3,'sport','UR::Value::Text','golf')"); ok(UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, infos => { is => 'URT::PersonInfo', reverse_as => 'person', is_many => 1 }, color => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'color'] }, species => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'species'] }, food => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'food'], is_optional => 1 }, sport => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'sport'], is_optional => 1 }, truelove => { is => 'URT::Person', via => 'infos', to => 'value_obj', where => [key => 'truelove'], is_optional => 1 }, ], ), 'Created class for main'); ok(UR::Object::Type->define( class_name => 'URT::PersonInfo', table_name => 'PERSON_INFO', data_source => 'URT::DataSource::SomeSQLite', id_by => [ pi_id => { is => 'Number' }, ], has => [ person => { is => 'URT::Person', id_by => 'person_id' }, key => { is => 'Text' }, value_class_name => { is => 'Text' }, value_id => { is => 'Text' }, value_obj => { is => 'UR::Object', id_class_by => 'value_class_name', id_by => 'value_id' }, ], ), "Created class for person_info"); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $thing; $query_count = 0; my $kermit = URT::Person->get(id => 1, -hints => ['color','species','food','sport','truelove']); ok($kermit, 'Got person 1'); is($query_count, 1, 'made 1 query'); $query_count = 0; is($kermit->name, 'Kermit', 'Name is Kermit'); is($query_count, 0, 'Made no queries for direct property'); $query_count = 0; is($kermit->color, 'green', 'Color is green'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->species, 'frog', 'species is frog'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->food, 'flies', 'food is fies'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->sport, undef, 'sport is undef'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->truelove, undef, 'truelove is undef'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; my $piggy = URT::Person->get(id => 2, -hints => ['color','sport']); ok($piggy, 'Got person 2'); is($query_count, 1, 'made 1 query'); $query_count = 0; is($piggy->name, 'Miss Piggy', 'Name is Miss Piggy'); is($query_count, 0, 'Made no queries for direct property'); $query_count = 0; is($piggy->color, 'pink', 'Color is pink'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($piggy->species, 'pig', 'species is pig'); is($query_count, 1, 'Made one query for indirect, non-hinted property'); $query_count = 0; is($piggy->food, undef, 'food is undef'); is($query_count, 1, 'Made one query for indirect, non-hinted property'); $query_count = 0; is($piggy->sport, 'karate', 'sport is karate'); is($query_count, 0, 'Made no queries for indirect, hinted property'); #$query_count = 0; #is($piggy->truelove, $kermit, 'truelove is kermit!'); #is($query_count, 0, 'Made no queries for indirect, hinted property'); sub unload_everything { for my $o (URT::PersonInfo->is_loaded()) { $o->unload } for my $o (URT::Person->is_loaded()) { $o->unload } my @loaded = URT::PersonInfo->is_loaded(); is(scalar(@loaded), 0, "no hangoff data loaded"); } my (@muppets, @loaded); unload_everything(); $query_count = 0; @muppets = URT::Person->get('truelove.id' => 1); is(scalar(@muppets), 1, "got one muppet that loves kermit"); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only loaded the object needed and the comparison object, and not the other object in the table (successfully wrote the where clause)"); unload_everything(); $kermit = URT::Person->get(1); $query_count = 0; @muppets = URT::Person->get('truelove' => $kermit); is(scalar(@muppets), 1, "got one muppet that loves kermit") or diag(\@muppets); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only found the new object and the parameter object in the cachee (succesffully wrote the where clause to exclude the other db data)"); unload_everything(); $kermit = URT::Person->get(1); $query_count = 0; @muppets = URT::Person->get('truelove.food' => 'flies'); is(scalar(@muppets), 1, "got one muppet that loves someone who eats flies") or diag(\@muppets); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source and beyond"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only found the new object and the parameter object in the cachee (succesffully wrote the where clause to exclude the other db data)"); 87f_via_property_joins_to_itself.t100664023532023421 365612544604517 21676 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 5; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; # The 'father_name' property of Person requires a join back to Person # Now that we don't index delegated properties, we should be able to # load the whole result in one query $dbh->do('create table person (person_id integer primary key not null, name varchar, father_id integer references person(person_id))'); # Bob is Fred's father. Bob doesn't have a father recorded in the table $dbh->do("insert into person values (1,'Bob', null)"); $dbh->do("insert into person values (2,'Fred', 1)"); # Mike is Joe's father $dbh->do("insert into person values (3,'Mike', null)"); $dbh->do("insert into person values (4,'Joe', 3)"); # Bob (no relation to the first Bob) is Frank's father, and Bubba is Bob's father $dbh->do("insert into person values (5,'Bubba', null)"); $dbh->do("insert into person values (6,'Bob', 5)"); $dbh->do("insert into person values (7,'Frank', 6)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => 'person_id', has => [ name => { is => 'String' }, father => { is => 'Person', id_by => 'father_id' }, father_name => { via => 'father', to => 'name' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = Person->get(father_name => 'Bob'); is(scalar(@p), 2, 'Got 2 people back'); is($p[0]->name, 'Fred', 'First is the right person'); is($p[1]->name, 'Frank', 'Second is the right person'); is($query_count, 1, 'Made one query'); 87g_doubly_delegated_multiple_pk_works.t100664023532023421 1460412544604517 23043 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Tests that the code to efficiently load data from double delegated properties # works properly when the linkage between classes involves multiple primary keys use URT; subtest 'via/reverse-as' => sub { # This is essentially the same test as 87_is_many_indirect_is_efficient.t, except # that the link between Car and CarPart has a 2-column foreign key. This means # the system needs to perform more than one query to collect the result objects plan tests => 7; ok(UR::Object::Type->define( class_name => 'URT::Person', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color' }, car_parts => { is => 'URT::CarParts', via => 'cars', to => 'parts', is_optional => 1, is_many => 1 }, car_parts_prices => { via => 'cars', to => 'parts_prices', is_optional => 1, is_many => 1 }, ], ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', id_by => [ car_id => { is => 'NUMBER' }, car_id2 => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, parts_prices => { via => 'parts', to => 'price', is_many => 1}, ], ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => ['car_id', 'car_id2'] }, ], ), "Created class for CarParts"); # Create some objects # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { my %args; @args{qw( person_id name is_cool )} = @$row; URT::Person->create(%args); } foreach my $row ( [ 1,1,'red',0,1], [ 2,2,'blue',1, 2], [3,3,'red',1,3],[4,4,'blue',1,4],[5,5,'yellow',1,1] ) { my %args; @args{qw( car_id car_id2 color is_primary owner_id )} = @$row; URT::Car->create(%args); } # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio foreach my $row ( [1, 'wheels', 100, 1,1], [2, 'engine', 200, 1,1], [3, 'wheels', 100, 2,2], [4, 'seats', 50, 2,2], [5, 'engine', 200, 3,3], [6, 'radio', 50, 3,3], [7, 'seats', 50, 4,4], [8, 'radio', 50, 4,4], [9, 'custom wheels', 200, 5,5], [10,'neon lights', 100, 5,5], ) { my %args; @args{qw( part_id name price car_id car_id2 )} = @$row; URT::CarParts->create(%args); } my $person = URT::Person->get(1); ok($person, 'Got person object'); my @colors = $person->cars(); is(scalar(@colors), 2, 'person has 2 cars with colors'); my @prices = $person->car_parts_prices(); is(scalar(@prices), 4, "person's cars have 4 car_parts with prices"); URT::CarParts->unload(); my @parts = $person->car_parts; my @parts_ids = sort { $a <=> $b } map { $_->id } @parts; is_deeply(\@parts_ids, [1, 2, 9, 10], 'Got the correct CarParts objects'); }; subtest 'via/via' => sub { # This is the same as the '"to" is via-to' test in 56c_via_property_with_order_by.t # except for multiple FK properties for value_obj plan tests => 1; UR::Object::Type->define( class_name => 'ViaThing', has_many => [ attribs => { is => 'ViaAttribute', reverse_as => 'thing' }, favorites => { via => 'attribs', to => 'value', where => [ key => 'favorite']},#, '-order_by' => 'rank' ] }, ], ); UR::Object::Type->define( id_by => ['id1', 'id2'], class_name => 'ViaValue', has => [ 'value' ], ); UR::Object::Type->define( class_name => 'ViaAttribute', has => [ thing => { is => 'ViaThing', id_by => 'thing_id' }, key => { is => 'String' }, value_obj => { is => 'ViaValue', id_by => ['value_obj_id', 'value_obj_id2'] }, value => { via => 'value_obj', to => 'value' }, rank => { is => 'Integer' }, ] ); # make a Thing with favorites 1, 2, 4 and 6, ranked in numerical order # but their IDs are not sorted the same as their values/ranks my $thing = ViaThing->create(); my @value_objs = do { my $id = 100; map { ViaValue->create(id1 => $id, id2 => $id++, value => $_) } (qw( 4 2 6 2 1)); }; my @attrib_objs = map { ViaAttribute->create( thing_id => $thing->id, key => 'favorite', value_obj_id => $_->id1, value_obj_id2 => $_->id2, rank => $_->value, ) } @value_objs; my @favorites = sort { $a <=> $b } $thing->favorites; is_deeply(\@favorites, [ 1, 2, 2, 4, 6], 'Got back ordered favorites', ); }; 89_loading_with_boolexpr_evaluate.t100664023532023421 460612544604517 22001 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); my $query_count; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $query_count = 0; # Since this query is against a calculate property, it will do a more # general query, import more data than is strictly necessary, and throw # out objects after loading them because they don't pass BoolExpr evaluation my @things = URT::Person->get(uc_name => 'lowercase'); is(scalar(@things), 0, 'No Persons with uc_name => "lowercase"'); is($query_count, 1, 'Made 1 query'); # This will actually trigger another DB query, though all the objects # it loads will already exist in the context. The underlying context # iterator needs to correctly throw away non-matching objects and # only return the one we're looking for $query_count = 0; @things = URT::Person->get(uc_name => 'FRED'); is(scalar(@things), 1, 'Got 1 thing with uc(name) FRED'); is($things[0]->name, 'Fred', 'Name is correct'); is($query_count, 1, 'Made 1 query'); sub create_db_tables { my $dbh = shift; ok($dbh->do('create table person ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created things table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ 'person_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'uc_name' => { calculate_from => 'name', calculate => 'uc($name)' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Person"); ok($dbh->do(q(insert into person (person_id, name) values (1, 'Bob'))), 'insert a person'); ok($dbh->do(q(insert into person (person_id, name) values (2, 'Joe'))), 'insert a person'); ok($dbh->do(q(insert into person (person_id, name) values (3, 'Fred'))), 'insert a person'); } 90_comparison_value_and_escape_character_to_regex.t100664023532023421 260312544604517 25134 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; use UR::BoolExpr::Template::PropertyComparison::Like; package Foo; class Foo { id_by => ['a'], has => [qw/a b c/], }; package main; for my $sc ('(', ')', '{', '}', '[', ']', '?', '.', '+', '|', '-') { my $value = "foo$sc"; my $esc = quotemeta($sc); my $expected_escaped_value = qr|^foo$esc$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly escaped $sc"); } { my $value = "foo%"; my $expected_escaped_value = qr|^foo.*$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly changed '%' to wildcard"); } { my $value = "foo_"; my $expected_escaped_value = qr|^foo.$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly changed '_' to wildcard"); } my $create_object = Foo->create(a => '0', b => 'foo)bar'); is(ref $create_object, 'Foo', 'created a Foo'); my $get_object = Foo->get('b like' => 'foo)%'); is(ref $get_object, 'Foo', 'got object that was just created using like with special char'); done_testing(); 91_object_sets.t100664023532023421 2712712544604517 16051 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_count => { via => 'car_set', to => 'count' }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, primary_car_uc_color => { via => 'primary_car', to => 'uc_color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, uc_color => { calculate_from => ['color'], calculate => q( return uc($color) ) }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my @c = URT::Car->get(owner_id => 13, "is_primary true" => 1); $query_count = 0; my $set = URT::Person->define_set('age <' => 20); ok($set, 'Defined set of people younger than 20'); is($query_count, 0, 'Made no queries'); $query_count = 0; my $count = $set->count(); is($count, 0, 'Set count is 0'); is($query_count, 1, 'Made 1 query'); $query_count = 0; is(scalar($set->members), undef, 'Set has no members'); is($query_count, 1, 'Made 1 query'); # the above query for count didn't actually retrieve the members $query_count = 0; $set = URT::Person->define_set(is_cool => 1); ok($set, 'Defined set of cool people'); is($query_count, 0, 'Made no queries'); $query_count = 0; $count = $set->count(); is($count, 3, '3 people are cool'); is($query_count, 1, 'Made 1 query'); $query_count = 0; is_deeply([ map { $_->name } $set->members], [qw(Bob Joe Frank)], 'Got the right members'); is($query_count, 1, 'Made one query'); # again, getting the count didn't load the members is_deeply([ map { $_->name } members_via_iterator($set)], [ map { $_->name } $set->members], 'Got the right members (via member_iterator)'); $query_count = 0; $set = URT::Person->define_set(); ok($set, 'Defined set of all people'); is($query_count, 0, 'Made no queries'); $query_count = 0; my @subsets = $set->group_by('car_colors'); is(scalar(@subsets), 4, 'Partitioning all people by car_colors yields 4 subsets'); is($query_count, 4, 'Made 4 queries'); # 3 to index the car_color for the 3 owners already loaded, 1 more for the group_by car_color # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my %people_by_car_color = ( 'red' => ['Bob', 'Mike'], 'blue' => ['Fred', 'Joe'], 'yellow' => ['Bob'], '' => ['Frank'], ); foreach my $subset ( @subsets ) { my $query_count = 0; my @colors = $subset->car_colors; is($#colors, 0, "one color returned") or diag "@colors"; my $color = shift @colors; is($query_count, 0, 'Getting car_colors from subset made no queries'); $query_count = 0; my @members = $subset->members(); is($query_count, 0, 'Getting members from subset made no queries'); my $expected_members = $people_by_car_color{$color || ''}; is(scalar(@members), scalar(@$expected_members), 'Got the expected number of subset members'); is_deeply([ map { $_->name } @members], $expected_members, 'Their names were correct'); @members = members_via_iterator($subset); is(scalar(@members), scalar(@$expected_members), 'Got the expected number of subset members (via member_iterator)'); is_deeply([ map { $_->name } @members], $expected_members, 'Their names were correct'); } $query_count = 0; $set = URT::Person->define_set(is_cool => 0); ok($set, 'Defined set of poeple that are not cool'); is($query_count, 0, 'Made no queries'); my %color_subsets; my %colors = ( 'pink' => [], 'red' => ['Mike'], 'blue' => ['Fred','Joe'] ); foreach my $color (keys %colors) { $query_count = 0; my $subset = $set->subset(primary_car_color => $color); ok($subset, "Defined a subset where primary_car_color is $color"); is($query_count, 0, 'Made no queries'); $color_subsets{$color} = $subset; } my $first_time = 1; foreach my $color ( keys %colors ) { my $subset = $color_subsets{$color}; $query_count = 0; my @names = $subset->name; my $expected_names = $colors{$color}; is(scalar(@names), scalar(@$expected_names), "Calling 'name' on the $color subset has the right number of names"); is_deeply(\@names, $expected_names, 'The names are correct'); is($query_count, 1, 'query count is correct'); $first_time = 0; } # Make a set that includes a filtered calculated property $query_count = 0; $set = URT::Car->define_set(uc_color => 'nomatches'); ok($set, 'Defined set of cars filtered by uc color that will not match anything'); is($query_count, 0, 'Made no queries'); is($set->count, 0, 'That set is empty'); ok($query_count, 'Made a query'); $query_count = 0; $set = URT::Person->define_set(primary_car_uc_color => 'wontmatch'); ok($set, 'Defined set of people filtered by uc color that will not match anything'); is($query_count, 0, 'Made no queries'); is($set->count, 0, 'That set is empty'); ok($query_count, 'Made a query'); # Test having an -order_by in addition to -group_by. It should throw an exception if # all the order_by columns don't appear in -group_by. # To mix it up a bit, we'll unload the peole with yellow cars. # This will require that it not do the sets entirely from cached objects. for my $person (URT::Person->is_loaded(car_colors => 'yellow')) { $person->unload; } # Now we'll delete the people with blue cars. This means we should not get a set # back even though the set is in the database. This will require that changes # check for intersecting sets and remove cached aggregate values. Because # we already track loaded object queries in a 2-tier hash (all_params_loaded), # we probably need a symmetrical structure for loaded sets to make this efficient. # This will ensure that, while creation and deletion must test all sets for membership, # updates will only need to look at sets with templates which involve the changed properties. #for my $person (URT::Person->is_loaded(car_colors => 'blue')) { # $person->delete; #} $query_count = 0; @subsets = URT::Person->get(-group_by => ['car_colors'], -order_by => ['car_colors']); is(scalar(@subsets), 4, 'Partitioning all people by car_colors yields 4 subsets, this time with order_by'); foreach (@subsets) { isa_ok($_, 'URT::Person::Set'); } is_deeply([ map { $_->car_colors } @subsets ], [undef, 'blue', 'red', 'yellow'], 'The color subsets were returned in the correct order'); is($query_count, 1, 'query count is correct'); @subsets = eval { URT::Person->get(-group_by => ['is_cool'], -order_by => ['car_colors'])}; is(scalar(@subsets), 0, 'Partitioning all people by is_cool, order_by car_colors returned no subsets'); like($@, qr(^Property 'car_colors' in the -order_by list must appear in the -group_by list), 'It threw the correct exception'); my $bob = URT::Person->get(name => 'Bob'); is($bob->car_count, 2, 'Bob has 2 cars using the set'); my $fred = URT::Person->get(name =>'Fred'); is($fred->car_count, 1, 'Fred has 1 car using the set'); my $frank = URT::Person->get(name => 'Frank'); is($frank->car_count, 0, 'Frank has 0 cars using the set'); do { # Class methods that are not implemented on the Set should be delegated # to the member class and should not be handled by the (immutable) # instance accesors. my $_some_member_method = ''; local *URT::Person::_some_member_method = sub { $_some_member_method = [@_] }; my $_some_member_method_can = URT::Person::Set->can('_some_member_method'); $@ = ''; eval { $_some_member_method_can->('URT::Person::Set', 42) }; my $error = $@; is($error, '', 'no error when calling _some_member_method on set class'); is_deeply( $_some_member_method, ['URT::Person', 42], '_some_member_method was delegated to member class' ); }; do { # Class methods that are implemented on the Set should be called as # normal and should not be handled by the (immutable) instance accesors. my $_some_set_method = 0; local *URT::Person::Set::_some_set_method = sub { $_some_set_method = [@_] }; my $_some_set_method_can = URT::Person::Set->can('_some_set_method'); $@ = ''; eval { $_some_set_method_can->('URT::Person::Set', 42) }; my $error = $@; is($error, '', 'no error when calling _some_set_method on set class'); is_deeply( $_some_set_method, ['URT::Person::Set', 42], '_some_set_method was not delegated to member class' ); }; do { # Instance methods should still be handled by the (immutable) member class # accessors. my $_some_member_method = ''; local *URT::Person::_some_member_method = sub { $_some_member_method = [@_] }; my $set = URT::Person->define_set(); my $_some_member_method_can = $set->can('_some_member_method'); $@ = ''; eval { $_some_member_method_can->($set, 42) }; my $error = $@; like($error, qr/_some_member_method/, 'got error when calling _some_member_method as a mutator on a set object'); }; done_testing(); sub members_via_iterator { my $set = shift; my $iter = $set->member_iterator(); my @members; while (my $m = $iter->next) { push @members, $m; } return @members; } 91b_sets_count_with_changes.t100664023532023421 3274112544604517 20616 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 125; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use Sub::Install; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); my $aggr_query_count = 0; my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { my ($observed, $aspect, $data) = @_; if ($data =~ /count|sum|min|max/) { $aggr_query_count++ } $query_count++; }), 'Created a subscription for query'); # Test creating/deleting/modifying objects that match extant sets. $query_count = 0; my $uncool_person_set = URT::Person->define_set(is_cool => 0); ok($uncool_person_set, 'Defined set of people that are not cool'); my $cool_person_set = URT::Person->define_set(is_cool => 1); ok($cool_person_set, 'Defined set of people that are cool'); is($cool_person_set->is_cool, 1, "access to a defining property works"); is($query_count, 0, 'Made no queries'); # Test set-relaying. my $car_set = $cool_person_set->cars_set; ok($car_set, "got a set of cars for the person set: object set -> value set"); # We're going to roll back all these changes just before the last block of tests my $t = UR::Context::Transaction->begin(); # Test aggregate function on a set that has no member changes. # All aggregate functions should trigger query since function is # performed server-side on the data source. { ok(!$cool_person_set->_members_have_changes, 'cool set has no changed objects'); $aggr_query_count = 0; is($cool_person_set->count, 3, '3 people are cool'); is($aggr_query_count, 1, 'count triggered one query'); $aggr_query_count = 0; is($cool_person_set->min('age'), 25, 'determined min age'); is($aggr_query_count, 1, 'min triggered one query'); $aggr_query_count = 0; is($cool_person_set->max('age'), 45, 'determined max age'); is($aggr_query_count, 1, 'max triggered one query'); $aggr_query_count = 0; is($cool_person_set->sum('age'), 110, 'determined the sum of all ages of the set'); is($aggr_query_count, 1, 'sum triggered one query'); } # Now induce a change in a member and ensure no queries are performed. { my $p = URT::Person->get(11); ok($cool_person_set->rule->evaluate($p), 'person is member of cool person set'); ok($p->age($p->age + 1), 'changed the age of the youngest person to be +1 (26)'); ok($cool_person_set->_members_have_changes, 'cool person set now has changes'); $aggr_query_count = 0; is($cool_person_set->count, 3, 'set membership count is still the same'); is($aggr_query_count, 0, 'count did not trigger query'); $aggr_query_count = 0; is($cool_person_set->min('age'), 26, 'minimum age is now 26'); is($aggr_query_count, 0, 'min did not trigger query'); $aggr_query_count = 0; is($cool_person_set->max('age'), 45, 'maximum age is still 45'); is($aggr_query_count, 0, 'max did not trigger query'); $aggr_query_count = 0; is($cool_person_set->sum('age'), 111, 'the sum of all ages is now 111'); is($aggr_query_count, 0, 'sum did not trigger query'); } # Now ensure that a set with same member class but without any actual # member changes is not affected. { is($uncool_person_set->member_class_name, $cool_person_set->member_class_name, 'sets have the same member class'); isnt($uncool_person_set, $cool_person_set, 'sets are not the same'); ok(!$uncool_person_set->_members_have_changes, 'uncool set has no changed objects'); $aggr_query_count = 0; is($uncool_person_set->count, 2, 'set membership count is still the same'); is($aggr_query_count, 1, 'count triggered one query'); $aggr_query_count = 0; is($uncool_person_set->min('age'), 30, 'minimum age is now 30'); is($aggr_query_count, 1, 'min triggered one query'); $aggr_query_count = 0; is($uncool_person_set->max('age'), 35, 'maximum age is still 35'); is($aggr_query_count, 1, 'max triggered one query'); $aggr_query_count = 0; is($uncool_person_set->sum('age'), 65, 'the sum of all ages is now 65'); is($aggr_query_count, 1, 'sum triggered one query'); } # Now ensure that changes to members are reflected in the set. { my $cool_person_count = $cool_person_set->count; my $jamesbond = URT::Person->create(name => 'James Bond', is_cool => 1, age => '35'); ok($jamesbond, 'Create a new cool person'); $aggr_query_count = 0; is($cool_person_set->count, $cool_person_count + 1, 'count increased'); is($aggr_query_count, 0, 'count did not trigger query'); my $fred = URT::Person->get(12); is($fred->is_cool, 0, 'fred is not cool (yet)'); $fred->is_cool(1); $aggr_query_count = 0; is($cool_person_set->count, $cool_person_count + 2, 'count increased again'); is($aggr_query_count, 0, 'count did not trigger query'); $aggr_query_count = 0; ok($jamesbond->delete, 'Delete James Bond'); is($cool_person_set->count, $cool_person_count + 1, 'count decreased after delete'); is($aggr_query_count, 0, 'Made no queries'); } # Fab up a method wrapper so we can tell if the accessor is called my $original_age_accessor = \&URT::Person::age; my $age_accessor_called = 0; Sub::Install::reinstall_sub({ into => 'URT::Person', as => 'age', code => sub { $age_accessor_called = 1; goto &$original_age_accessor } }); my $original_name_accessor = \&URT::Person::name; my $name_accessor_called = 0; Sub::Install::reinstall_sub({ into => 'URT::Person', as => 'name', code => sub { $name_accessor_called = 1; goto &$original_name_accessor } }); # Make a change, then do a set aggregate on a different property # it should do a single aggregate query on the DB and not load all # members ok($t->rollback(), 'Rollback changes'); $t = UR::Context::Transaction->begin(); { ok(URT::Person->unload(), 'Unload all Person objects'); my $p = URT::Person->get(11); is(scalar(@{[URT::Person->is_loaded]}), 1, 'One Person object is loaded'); $aggr_query_count = 0; is($cool_person_set->count, 3, 'set membership count is still the same'); is($aggr_query_count, 1, 'count made an aggregate query'); is(scalar(@{[URT::Person->is_loaded]}), 1, 'Still, one Person object is loaded'); $aggr_query_count = $age_accessor_called = 0; is($cool_person_set->sum('age'), 110, 'Get sum(age)'); is($aggr_query_count, 1, 'count made an aggregate query'); is($age_accessor_called, 0, '"age" accessor was not called'); is(scalar(@{[URT::Person->is_loaded]}), 1, 'Still, one Person object is loaded'); ok($cool_person_set->rule->evaluate($p), 'person is member of cool person set'); ok($p->name('AAAA'), 'changed the name of the person to AAAA'); ok($cool_person_set->_members_have_changes, 'cool person set now has changes'); # After changing the name, count and sum should still be valid cached values $aggr_query_count = 0; is($cool_person_set->count, 3, 'set membership count is still the same'); is($aggr_query_count, 0, 'count did not trigger query'); $aggr_query_count = 0; is($cool_person_set->sum('age'), 110, 'Get sum(age)'); is($aggr_query_count, 0, 'sum did not trigger query'); is($age_accessor_called, 0, '"age" accessor was not called'); $aggr_query_count = 0; is($cool_person_set->min('age'), 25, 'Minimum age is 25'); is($age_accessor_called, 0, "'age' accessor was not called"); # ran in the DB is($aggr_query_count, 1, 'Did one aggregate query'); is(scalar(@{[URT::Person->is_loaded]}), 1, 'Still, one Person object is loaded'); $aggr_query_count = 0; is($cool_person_set->min('name'), 'AAAA', 'Minimum name is AAAA'); is($aggr_query_count, 0, 'Made no aggregate queries'); is(scalar(@{[URT::Person->is_loaded]}), 3, 'All 3 Person objects were loaded that are is_cool'); # After changing the name, the min(age) value should still be cached $age_accessor_called = $aggr_query_count = 0; is($cool_person_set->min('age'), 25, 'Minimum age is 25'); is($age_accessor_called, 0, "'age' accessor was not called"); is($aggr_query_count, 0, 'Did no aggregate queries'); # Now, change age, and test that it has to re-calculate the age-dependant aggregates # but not the name-related aggregate ok($p->age(26), 'Change person age to 26'); $age_accessor_called = 0; is($cool_person_set->sum('age'), 111, 'Get sum(age)'); is($aggr_query_count, 0, 'sum did not trigger query'); is($age_accessor_called, 1, '"age" accessor was called'); $age_accessor_called = 0; is($cool_person_set->min('age'), 26, 'Minimum age is 26'); is($age_accessor_called, 1, "'age' accessor was called"); $name_accessor_called = 0; is($cool_person_set->min('name'), 'AAAA', 'Minimum name is AAAA'); is($name_accessor_called, 0, "'name' accessor was not called"); } # Test that changing set membership invalidates the whole cache of aggregate values ok($t->rollback(), 'Rollback changes'); $t = UR::Context::Transaction->begin(); { my $p = URT::Person->get(11); is($cool_person_set->min('age'), 25, 'Minimum age is 25'); is($cool_person_set->sum('age'), 110, 'Get sum(age)'); is($cool_person_set->min('name'), 'Bob', 'Minimum name is Bob'); ok(defined($p->is_cool(0)), 'Set person to be not cool'); $age_accessor_called = 0; is($cool_person_set->min('age'), 40, 'Minimum cool age is 40'); is($age_accessor_called, 1, "'age' accessor was called"); $age_accessor_called = 0; is($cool_person_set->sum('age'), 85, 'Get cool sum(age)'); is($age_accessor_called, 1, '"age" accessor was called'); $name_accessor_called = 0; is($cool_person_set->min('name'), 'Frank', 'Minimum cool name is Frank'); is($name_accessor_called, 1, "'name' accessor was called"); } # Test that changing a property on an object of the same class, but not a member # of the set, does not invalidate cached aggregate values ok($t->rollback(), 'Rollback changes'); { my $p = URT::Person->get(12); is($p->is_cool, 0, 'Got an uncool person'); is($cool_person_set->min('age'), 25, 'Minimum cool age is 25'); is($cool_person_set->sum('age'), 110, 'Get cool sum(age)'); is($cool_person_set->min('name'), 'Bob', 'Minimum cool name is Bob'); ok($p->age(99), "Change uncool person's age"); ok($p->name('goo'), "Change uncool person's name"); my $check_aggrs = sub { $age_accessor_called = 0; is($cool_person_set->min('age'), 25, 'Minimum cool age is 25'); is($age_accessor_called, 0, '"age" accessor was not called'); $age_accessor_called = 0; is($cool_person_set->sum('age'), 110, 'Get cool sum(age)'); is($age_accessor_called, 0, '"age" accessor was not called'); $name_accessor_called = 0; is($cool_person_set->min('name'), 'Bob', 'Minimum cool name is Bob'); is($name_accessor_called, 0, '"name" accessor not called'); }; $check_aggrs->(); ok($p->delete, 'Delete the uncool person'); $check_aggrs->(); ok(URT::Person->create(is_cool => 0, name => 'Porky Pig', age => 60), 'Create a new uncool person'); $check_aggrs->(); } 91c_set_relay.t100664023532023421 1266412544604517 15677 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 15; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table CAR_ENGINE (engine_id int NOT NULL PRIMARY KEY, car_id integer references CAR(car_id), size number)'), 'created car_engine table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car_engine values (?,?,?)'); foreach my $row ( [100, 1, 350], [ 200, 2, 400], [300, 3, 428], [400, 4, 429], [500, 5, 289] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'created a subscription for query'); #$DB::single = 1; my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'cars.color' => 'red', 'cars.engine.size' => 428, 'cars.is_primary true' => 1, ); my $s1 = URT::Person->define_set($bx1); ok($s1, "made an initial set $s1"); my $bx1r1 = $bx1->reframe('primary_car')->normalize; my $s2 = $s1->primary_car_set; is($s2->id, $bx1r1->id, "the expected reframed id on related set $s2"); my $bx1r2 = $bx1->reframe('primary_car.engine')->normalize; my $s3 = $s2->engine_set; is($s3->id, $bx1r2->id, "the expected reframed id on related set $s3"); my $s5 = $s1->__related_set__('cars.engine'); is($s5->id, $s3->id, "reframed set two steps away persons's cars.engine"); my $s6 = $s5->car_set->owner_set; ok($s6, "went back from the engine set to the car to the owner"); is($s6->id, $s1->id, "the owner set from the engine matches the original"); #$DB::single = 1; my $bx4 = $s2->rule->reframe("color"); ok($bx4, "got color reframe $bx4"); my $bx7 = URT::Car::Engine->define_boolexpr('car.owner_id' => 1234); my $bx7r = $bx7->reframe('car.owner'); #$DB::single = 1; my $z1 = URT::Car->define_boolexpr("color" => "red"); print "$z1\n"; my $z2 = $z1->reframe("owner"); print "$z2\n"; my $z4 = $z1->reframe("engine"); print "$z4\n"; my $z3 = $z1->reframe("color"); print "$z3\n"; __END__ my $s4 = $s2->color_set(); ok($s4, "got a set of colors: $s4"); __END__ note("******** or *********"); my $bx5 = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['primary_car.color' => 'red'], ] ); ok($bx5, "created an 'or' boolexpr $bx5"); my $s5 = URT::Person->define_set($bx5); ok($s5, "made a set for it $s5"); is($s5->rule->id, $bx5->id, "id is correct"); my $bx6 = $bx5->reframe('primary_car'); ok($bx6, "$bx6"); my $s6 = $s5->primary_car_set(); ok($s6, "$s6"); is($s6->id, $bx6->id, "id is correct"); 91d_basic_set.t100664023532023421 431012544604517 15612 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 28; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; ok(UR::Object::Type->define( class_name => 'URT::ThingNoDataSource', id_by => [ name => { is => 'String' }, ], has => [ group_name => { is => 'String' }, total_size => { is => 'Integer' }, ], ), 'Define class without a data source'); ok(UR::Object::Type->define( class_name => 'URT::ThingWithDataSource', id_by => [ name => { is => 'String' }, ], has => [ group_name => { is => 'String' }, total_size => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ), 'Define class with a data source'); my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table things (name varchar not null primary key, group_name varchar, total_size integer)'); foreach my $test_class ( 'URT::ThingNoDataSource', 'URT::ThingWithDataSource' ) { ok($test_class->create(name => 'a', group_name => '1', total_size => 10), "create $test_class a"); ok($test_class->create(name => 'b', group_name => '1', total_size => 20), "create $test_class b"); ok($test_class->create(name => 'c', group_name => '2', total_size => 30), "create $test_class c"); ok($test_class->create(name => 'd', group_name => '2', total_size => 40), "create $test_class d"); #my @sets = $test_class->get(-group_by => ['group_name'], -order_by => ['group_name'] ); my @sets = $test_class->define_set()->group_by('group_name'); is(scalar(@sets), 2, 'Got two sets back grouped by group_name'); is($sets[0]->group_name, '1', 'Group name 1 is first'); is($sets[0]->min('total_size'), 10, '10 is min total_size'); is($sets[0]->max('total_size'), 20, '20 is max total_size'); is($sets[0]->sum('total_size'), 30, '30 is sum total_size'); is($sets[1]->group_name, '2', 'Disk group 2 is second'); is($sets[1]->min('total_size'), 30, '30 is min total_size'); is($sets[1]->max('total_size'), 40, '40 is max total_size'); is($sets[1]->sum('total_size'), 70, '70 is sum total_size'); } 91e_via_set.t100664023532023421 576612544604517 15331 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # We found a case where a set would do the correct SQL query to get the count # but then would store the count on an alternate set. That alternate set was # constructed by the aggregate logic with a flattened, normalized rule instead # of the calling set's rule which would result in an undef count. setup(); my $show = URT::Show->get(name => 'Three Stooges'); is(URT::Actor->define_set(shows => $show)->count, 3); is(URT::Actor::Set->get(shows => $show)->count, 3); my $actor = URT::Actor->get(name => 'Larry'); is(URT::Show->define_set(actors => $actor)->count, 1); is(URT::Show::Set->get(actors => $actor)->count, 1); sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do(q(create table show (id integer PRIMARY KEY, name text))); $dbh->do(q(create table actor (id integer PRIMARY KEY, name text))); $dbh->do(q(create table show_actor_bridge (show_id integer, actor_id integer))); $dbh->do(q(insert into show values (1, 'Three Stooges'))); $dbh->do(q(insert into show values (2, 'Power Rangers'))); $dbh->do(q(insert into actor values (1, 'Larry'))); $dbh->do(q(insert into actor values (2, 'Curly'))); $dbh->do(q(insert into actor values (3, 'Moe'))); $dbh->do(q(insert into actor values (4, 'Black'))); $dbh->do(q(insert into show_actor_bridge values (1, 1))); $dbh->do(q(insert into show_actor_bridge values (1, 2))); $dbh->do(q(insert into show_actor_bridge values (1, 3))); $dbh->do(q(insert into show_actor_bridge values (2, 4))); $dbh->commit(); UR::Object::Type->define( class_name => 'URT::Show', id_by => 'id', has => [ name => { is => 'Text' }, actor_bridges => { is => 'URT::ShowActorBridge', reverse_as => 'show', is_many => 1 }, actors => { is => 'URT::Actor', via => 'actor_bridges', to => 'actor', is_many => 1 }, ], table_name => 'show', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Actor', id_by => 'id', has => [ name => { is => 'Text' }, show_bridges => { is => 'URT::ShowActorBridge', reverse_as => 'actor', is_many => 1 }, shows => { is => 'URT::Show', via => 'show_bridges', to => 'show', is_many => 1 }, ], table_name => 'actor', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::ShowActorBridge', id_by => [ show => { is => 'URT::Show', id_by => 'show_id', }, actor => { is => 'URT::Actor', id_by => 'actor_id', }, ], table_name => 'show_actor_bridge', data_source => 'URT::DataSource::SomeSQLite', ); } 92_copy_loaded_objects_to_alternate_db.t100664023532023421 3772212544604517 22751 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tBEGIN { # This test requires committing to be enabled delete $ENV{UR_DBI_NO_COMMIT}; } use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 91; use URT::DataSource::SomeSQLite; use File::Temp; use File::Spec; # Make a few couple classes attached to a data source. Load some of the objects. # The data should be copied to the test database fill_primary_db(); setup_classes(); foreach my $no_commit ( 0, 1 ) { diag("no_commit $no_commit"); UR::DBI->no_commit($no_commit); diag('sqlite file'); my $db_file = load_objects_fill_file(); test_results_db_file($db_file); diag('sqlite directory'); my $db_dir = load_objects_fill_dir(); test_results_db_dir($db_dir); } sub fill_primary_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('PRAGMA foreign_keys = ON'); # "simple" is a basic table, no inheritance or hangoffs ok($dbh->do('create table simple (simple_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create table simple'); my $sth = $dbh->prepare('insert into simple (simple_id, name) values (?,?)') || die "prepare simple: $DBI::errstr"; foreach my $row ( [1, 'use'], [2, 'ignore'] ) { $sth->execute(@$row) || die "execute simple: $DBI::errstr"; } $sth->finish; # "parent" and "child" tables with inheritance ok($dbh->do('create table parent (parent_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create table parent'); ok($dbh->do('create table child (child_id integer NOT NULL PRIMARY KEY REFERENCES parent(parent_id), data varchar)'), 'create table child'); $sth = $dbh->prepare('insert into parent (parent_id, name) values (?,?)') || die "prepare parent: $DBI::errstr"; foreach my $row ( [1, 'use'], [2, 'ignore']) { $sth->execute(@$row) || die "execute parent: $DBI::errstr"; } $sth->finish; $sth = $dbh->prepare('insert into child (child_id, data) values (?,?)') || die "prepare child: $DBI::errstr"; foreach my $row ( [1, 'child data 1'], [2, 'child data 2'] ) { $sth->execute(@$row) || die "execute child: $DBI::errstr"; } $sth->finish; # "obj" and "hangoff" tables ok($dbh->do('create table obj (obj_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create table obj'); ok($dbh->do('create table hangoff (hangoff_id integer NOT NULL PRIMARY KEY, value varchar, obj_id integer REFERENCES obj(obj_id))'), 'create table hangoff'); $sth = $dbh->prepare('insert into obj (obj_id, name) values (?,?)') || die "prepare obj: $DBI::errstr"; foreach my $row ( [1, 'use'], [2, 'ignore'], [3, 'keep'] ) { $sth->execute(@$row) || die "execute hangoff: $DBI::errstr"; } $sth->finish; $sth = $dbh->prepare('insert into hangoff (hangoff_id, value, obj_id) values (?,?,?)') || die "prepare hangoff: $DBI::errstr"; foreach my $row ( [1, 'use', 1], [2, 'ignore', 2], [3, 'keep', 3] ) { $sth->execute(@$row) || die "execute obj: $DBI::errstr"; } $sth->finish; # data and data_attribute tables ok($dbh->do('create table data (data_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create table data'); ok($dbh->do('create table data_attribute (data_id integer, name varchar, value varchar, PRIMARY KEY (data_id, name, value))'), 'create table data_attribute'); $sth = $dbh->prepare('insert into data (data_id, name) values (?,?)') || die "prepare data: $DBI::errstr"; foreach my $row ( [ 1, 'use'], [2, 'ignore'], [3, 'use'] ) { $sth->execute(@$row) || die "execute data: $DBI::errstr"; } $sth->finish; $sth = $dbh->prepare('insert into data_attribute (data_id, name, value) values (?,?,?)') || die "prepare data_attribute: $DBI::errstr"; # data_id 3 has no data_attributes foreach my $row ( [1, 'coolness', 'high'], [1, 'foo', 'bar'], [2, 'coolness', 'low']) { $sth->execute(@$row) || die "execute data_attribute: $DBI::errstr"; } $sth->finish; # a table that references itself ok($dbh->do('create table self_reference (sr_id integer NOT NULL PRIMARY KEY, prev_id integer REFERENCES self_reference(sr_id), name varchar)'), 'create table self_reference'); $sth = $dbh->prepare('insert into self_reference (sr_id, prev_id, name) values (?,?,?)'); foreach my $row ( [1, undef, 'use parent'], [2, 1, 'use'], [3, undef, 'ignore parent'], [4, 3, 'ignore']) { $sth->execute(@$row) || die "execute self_reference: $DBI::errstr"; } # Entities and relationships ok($dbh->do('create table entity (entity_id integer NOT NULL PRIMARY KEY, name VARCHAR)'), 'create entity table'); ok($dbh->do('create table relationship (from_entity_id integer NOT NULL REFERENCES entity(entity_id), to_entity_id integer NOT NULL REFERENCES entity(entity_id), label varchar, PRIMARY KEY (from_entity_id, to_entity_id))'), 'create entity relationship table'); $sth = $dbh->prepare('insert into entity (entity_id, name) values (?,?)'); foreach my $row ( [1, 'use parent'], [2, 'ignore parent'], [3, 'use child'], [4, 'ignore child'] ) { $sth->execute(@$row) || die "execute entity insert: $DBI::errstr"; } $sth = $dbh->prepare('insert into relationship (from_entity_id, to_entity_id, label) values (?,?,?)'); foreach my $row ( [1,3,'use'], [2,4,'ignore']) { $sth->execute(@$row) || die "execute relationship insert: $DBI::errstr"; } # subclassable hangoff data ok($dbh->do('create table obj_with_subclassable_hangoff (obj_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create table obj_with_subclassable_hangoff'); ok($dbh->do('create table subclassable_hangoff (hangoff_id integer NOT NULL PRIMARY KEY, value varchar, obj_id integer REFERENCES obj(obj_id), subclass_name varchar NOT NULL)'), 'create table subclassable_hangoff'); $sth = $dbh->prepare('insert into obj_with_subclassable_hangoff (obj_id, name) values (?,?)') || die "prepare obj_with_subclassable_hangoff: $DBI::errstr"; foreach my $row ( [1, 'use'], [2, 'ignore'], [3, 'keep'] ) { $sth->execute(@$row) || die "execute hangoff: $DBI::errstr"; } $sth->finish; $sth = $dbh->prepare('insert into subclassable_hangoff (hangoff_id, value, obj_id, subclass_name) values (?,?,?,?)') || die "prepare subclassable_hangoff: $DBI::errstr"; foreach my $row ( [1, 'use', 1, 'URT::SubclassedHangoff'], [2, 'ignore', 2, 'URT::SubclassedHangoff'], [3, 'keep', 3, 'URT::SubclassedHangoff'] ) { $sth->execute(@$row) || die "execute obj: $DBI::errstr"; } $sth->finish; ok($dbh->commit(), 'Commit initial database state'); } sub setup_classes { UR::Object::Type->define( class_name => 'URT::Simple', id_by => 'simple_id', has => ['name'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'simple', ); UR::Object::Type->define( class_name => 'URT::Parent', id_by => 'parent_id', has => ['name'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'parent', ); UR::Object::Type->define( class_name => 'URT::Child', is => 'URT::Parent', id_by => 'child_id', has => ['data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'child', ); UR::Object::Type->define( class_name => 'URT::Obj', id_by => 'obj_id', has => [ name => { is => 'String' }, hangoff => { is => 'URT::Hangoff', reverse_as => 'obj', is_many => 1 }, hangoff_value => { via => 'hangoff', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'obj', ); UR::Object::Type->define( class_name => 'URT::Hangoff', id_by => 'hangoff_id', has => [ value => { is => 'String' }, obj => { is => 'URT::Obj', id_by => 'obj_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'hangoff', ); UR::Object::Type->define( class_name => 'URT::Data', id_by => 'data_id', has => [ name => { is => 'String' }, attributes => { is => 'URT::DataAttribute', reverse_as => 'data', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'data', ); UR::Object::Type->define( class_name => 'URT::DataAttribute', id_by => ['data_id', 'name', 'value' ], has => [ data => { is => 'URT::Data', id_by => 'data_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'data_attribute', ); UR::Object::Type->define( class_name => 'URT::SelfReferencing', id_by => 'sr_id', has => [ prev => { is => 'URT::SelfReferencing', id_by => 'prev_id', is_optional => 1 }, name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'self_reference', ); UR::Object::Type->define( class_name => 'URT::Entity', id_by => 'entity_id', has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'entity', ); UR::Object::Type->define( class_name => 'URT::Relationship', id_by => ['from_entity_id','to_entity_id'], has => [ label => { is => 'String' }, from_entity => { is => 'URT::Entity', id_by => 'from_entity_id' }, to_entity => { is => 'URT::Entity', id_by => 'to_entity_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'relationship', ); UR::Object::Type->define( class_name => 'URT::ObjWithSubclassedHangoff', id_by => 'obj_id', has => [ name => { is => 'String' }, hangoff => { is => 'URT::SubclassedHangoff', reverse_as => 'obj', is_many => 1 }, hangoff_value => { via => 'hangoff', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'obj_with_subclassable_hangoff', ); UR::Object::Type->define( class_name => 'URT::SubclassableHangoff', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'hangoff_id', has => [ value => { is => 'String' }, obj => { is => 'URT::ObjWithSubclassedHangoff', id_by => 'obj_id' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'subclassable_hangoff', ); UR::Object::Type->define( class_name => 'URT::SubclassedHangoff', is => 'URT::SubclassableHangoff', ); } sub load_objects_fill_file { my $temp_db_file = File::Temp->new(); $temp_db_file->close(); URT::DataSource::SomeSQLite->alternate_db_dsn('dbi:SQLite:dbname='.$temp_db_file->filename); _load_objects(); URT::DataSource::SomeSQLite->alternate_db_dsn(''); return $temp_db_file; } sub _load_objects { ok(scalar(URT::Simple->get(name => 'use')), 'Get simple object'); ok(scalar(URT::Child->get(name => 'use')), 'Get child object'); my @got = URT::Obj->get(hangoff_value => 'use'); ok(scalar(@got), 'Get obj with hangoff'); ok(scalar(URT::Hangoff->get(value => 'keep')), 'Get hangoff data directly'); @got = URT::Data->get(name => 'use', -hints => 'attributes'); ok(scalar(@got), 'Get data and and data attributes'); ok(scalar(URT::SelfReferencing->get(name => 'use')), 'Get object via self-referencing table'); ok(scalar(URT::Relationship->get(label => 'use')), 'Get relationship with two PKs'); @got = URT::ObjWithSubclassedHangoff->get(hangoff_value => 'use'); ok(scalar(@got), 'Get obj with subclassed hangoff'); # The Obj is a prerequisite of the Hangoff. Create the Obj with a dummy ID, which won't # be inserted to the alternate DB, which means the Hangoff can't be inserted either. eval { UR::DBI->no_commit(1); my $obj = URT::Obj->create(id => 999, name => 'use'); ok($obj, 'Create URT::Obj with dummy IDs on'); my $hangoff = URT::Hangoff->create(value => 'use', obj => $obj, id => $$); UR::Context->commit(); UR::Context->current->reload('URT::Obj', hangoff_value => 'use'); UR::Context->rollback; $obj->delete; # need delete because no-commit is on inside here }; UR::DBI->no_commit(0); $_->unload() foreach ( qw( URT::Simple URT::Child URT::Obj URT::Hangoff URT::Data URT::DataAttribute URT::SelfReferencing URT::Entity URT::Relationship URT::ObjWithSubclassedHangoff URT::SubclassableHangoff ) ); } sub test_results_db_file { my $db_file = shift; my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file",'',''); $dbh->{FetchHashKeyName} = 'NAME_lc'; my $simple = $dbh->selectall_hashref('select * from simple', 'simple_id'); is_deeply($simple, { 1 => { simple_id => 1, name => 'use' } }, 'simple table created with correct column names'); my $parent = $dbh->selectall_hashref('select * from parent', 'parent_id'); is_deeply($parent, { 1 => { parent_id => 1, name => 'use' } }, 'table parent'); my $child = $dbh->selectall_hashref('select * from child', 'child_id'); is_deeply($child, { 1 => { child_id => 1, data => 'child data 1' } }, 'table child'); my $obj = $dbh->selectall_hashref('select * from obj', 'obj_id'); is_deeply($obj, { 1 => { obj_id => 1, name => 'use' }, 3 => { obj_id => 3, name => 'keep' }, }, 'table obj'); my $hangoff = $dbh->selectall_hashref('select * from hangoff', 'hangoff_id'); is_deeply($hangoff, { 1 => { hangoff_id => 1, obj_id => 1, value => 'use' }, 3 => { hangoff_id => 3, obj_id => 3, value => 'keep'}, }, 'table hangoff'); my $data = $dbh->selectall_hashref('select * from data', 'data_id'); is_deeply($data, { 1 => { data_id => 1, name => 'use' }, 3 => { data_id => 3, name => 'use' }, }, 'table data'); my $data_attribute = $dbh->selectall_hashref('select * from data_attribute', 'name'); is_deeply($data_attribute, { coolness => { data_id => 1, name => 'coolness', value => 'high' }, foo => { data_id => 1, name => 'foo', value => 'bar' } }, 'table data_attribute' ); my $self_referencing = $dbh->selectall_hashref('select * from self_reference', 'name'); is_deeply($self_referencing, { 'use parent' => { sr_id => 1, prev_id => undef, name => 'use parent' }, use => { sr_id => 2, prev_id => 1, name => 'use' }, }, 'table self_referencing'); my $entities = $dbh->selectall_hashref('select * from entity', 'entity_id'); is_deeply($entities, { 1 => { entity_id => 1, name => 'use parent' }, 3 => { entity_id => 3, name => 'use child' }, }, 'table entity', ); my $relationships = $dbh->selectall_hashref('select * from relationship', 'from_entity_id'); is_deeply($relationships, { 1 => { from_entity_id => 1, to_entity_id => 3, label => 'use'} }, 'table relationship', ); } sub load_objects_fill_dir { my $temp_db_dir = File::Temp::tempdir( CLEANUP => 1 ); URT::DataSource::SomeSQLite->alternate_db_dsn('dbi:SQLite:dbname='.$temp_db_dir); _load_objects(); URT::DataSource::SomeSQLite->alternate_db_dsn(''); return $temp_db_dir; } sub test_results_db_dir { my $temp_db_dir = shift; my $main_schema_file = File::Spec->catfile($temp_db_dir, 'main.sqlite3'); ok(-f $main_schema_file, 'main schema file main.sqlite3'); test_results_db_file($main_schema_file); } 92_save_object_with_propertyless_column.t100664023532023421 402312544604517 23243 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # Make a class attached to a table where some columns in the table have # no associated property. Test that we can CRUD my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh->do("create table foo (foo_id integer NOT NULL PRIMARY KEY, name varchar, missing varchar)"), 'create table'); ok($dbh->do("insert into foo values (100,'DeleteMe', 'blah')"), 'insert row'); ok($dbh->do("insert into foo values (101,'UpdateMe', 'blah')"), 'insert row'); UR::Object::Type->define( class_name => 'URT::Foo', id_by => 'foo_id', has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'foo', ); my $obj = URT::Foo->get(name => 'DeleteMe'); ok($obj, 'Got an object'); ok($obj->delete(), 'Called delete()'); $obj = URT::Foo->get(name => 'UpdateMe'); ok($obj, 'Got a second object'); ok($obj->name('Updated'), 'Changed its name'); $obj = URT::Foo->create(name => 'Created'); ok($obj, 'Created an object'); my $new_object_id = $obj->id; my $commit = eval { UR::Context->commit() }; ok($commit, 'commit'); ok(! $@, 'No exceptions during commit'); diag($@) if $@; my @row = $dbh->selectrow_array('select foo_id,name,missing from foo where foo_id = 100'); ok(!scalar(@row), 'Deleted object was deleted from database'); @row = $dbh->selectrow_array('select foo_id,name,missing from foo where foo_id = 101'); ok(scalar(@row), 'Found row in database for updated object'); is($row[1], 'Updated', 'name column was updated correctly'); is($row[2], 'blah', 'missing column was not touched'); @row = $dbh->selectrow_array("select foo_id,name,missing from foo where foo_id = $new_object_id"); ok(scalar(@row), 'Found row in database for created object'); is($row[1], 'Created', 'name column is correct'); is($row[2], undef, 'missing column is correctly NULL/undef'); 93_namespace.t100664023532023421 417612544604517 15462 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; if ($^O eq 'darwin') { plan skip_all => 'known to fail OS X' } else { plan tests => 31 } is(URT->class, 'URT', 'Namespace name'); my $class_meta = URT->get_member_class('URT::Thingy'); ok($class_meta, 'get_member_class'); is($class_meta->class_name, 'URT::Thingy', 'get_member_class returned the right class'); # This is basically the list of Perl modules under URT/ # note that the 38* classes do not compile because they use data sources that exist # only during that test, and so are not returned by get_material_classes() my @expected_class_names = map { 'URT::' . $_ } qw( 34Baseclass 34Subclass 43Primary 43Related Context::Testing DataSource::CircFk DataSource::Meta DataSource::SomeFile DataSource::SomeFileMux DataSource::SomeMySQL DataSource::SomeOracle DataSource::SomePostgreSQL DataSource::SomeSQLite ObjWithHash RAMThingy Thingy Vocabulary ); my @class_metas = URT->get_material_classes; is(scalar(@class_metas), scalar(@expected_class_names), 'get_material_classes returned expected number of items'); foreach (@class_metas) { isa_ok($_, 'UR::Object::Type'); } my @class_names = sort map { $_->class_name } @class_metas; is_deeply(\@class_names, \@expected_class_names, 'get_material_classes'); my @data_sources = sort URT->get_data_sources; foreach ( @data_sources) { isa_ok($_, 'UR::DataSource'); } my @expected_ds_names = map { 'URT::' . $_ } qw( DataSource::CircFk DataSource::Meta DataSource::SomeFile DataSource::SomeFileMux DataSource::SomeMySQL DataSource::SomeOracle DataSource::SomePostgreSQL DataSource::SomeSQLite ); my @data_source_names = sort map { $_->class } @data_sources; is_deeply(\@data_source_names, \@expected_ds_names, 'get_data_sources'); 93b_namespace_loaded_from_symlink.t100664023532023421 167412544604517 21725 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 7; use Cwd; use File::Temp; use IO::File; # Test the condition when one of the directories in @INC is a symlink, load a Namespace # module from that directory, and make sure the entry in @INC and %INC have turned that # path into an absolute path my $temp_dir = File::Temp::tempdir(CLEANUP => 1); ok($temp_dir, 'Create temp directory to hold symlink'); my $dir = Cwd::abs_path(File::Basename::dirname(__FILE__) . '/../../'); ok(-f $dir.'/Slimspace.pm', 'Found Slimspace.pm'); my $inc_dir = $temp_dir .'/inc'; ok(symlink($dir, $inc_dir), 'Create symlink'); unshift @INC, $inc_dir; is($INC[0], $inc_dir, 'First in \@INC is the temp dir synlink'); use_ok('Slimspace'); my $path = $INC{'Slimspace.pm'}; my $abs_path = Cwd::abs_path($path); is($path, $abs_path, '\%INC for Slimspace.pm is the absolute path'); is($INC[0], $dir, 'First in \@INC was rewritten to be absolute path'); 94_chain_join.t100664023532023421 1356312544604517 15650 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # the initial code is from test 91b, to set-up some joinable data use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table CAR_ENGINE (engine_id int NOT NULL PRIMARY KEY, car_id integer references CAR(car_id), size number)'), 'created car_engine table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1], is_optional => 1 }, # direct where big_cars => { is => 'URT::Car', via => 'cars', to => '__self__', where => [ 'engine_size >=' => 400 ], }, # indirect where car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, engine_size => { is => 'Number', via => 'engine', to => 'size' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car_engine values (?,?,?)'); foreach my $row ( [100, 1, 350], [ 200, 2, 400], [300, 3, 428], [400, 4, 429], [500, 5, 289] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); #$DB::single = 1; # chain property equiv my $bx1 = URT::Person->define_boolexpr('primary_car.color' => 'red'); ok($bx1, "got bx with property chain"); my @p1 = URT::Person->get('primary_car.color' => 'red'); is(scalar(@p1), 1, "got one person with a primary car color of red using a property chain"); my @p2 = URT::Person->get('primary_car_color' => 'red'); is(scalar(@p2),1,"got one person with a primary car color of red using a custom accessor"); is($p1[0], $p2[0], "result matches"); my @p3 = URT::Person->get('primary_car.color' => ['red']); is(scalar(@p3), 1, "got one person with a primary car color of red using a property chain and the \"in\" operator"); my $bx5 = URT::Person->define_boolexpr('cars.color' => 'blue', 'cars.engine.size' => '400'); #print "$bx5"; #$ENV{UR_DBI_MONITOR_SQL} = 1; $DB::single = 1; my @p5 = URT::Person->get($bx5); ok("@p5", "regular query works for " . scalar(@p5) . " objects"); __END__ my $bx4i = URT::Person->define_boolexpr('big_cars.color' => 'red'); my $bx4f = $bx4i->flatten; print "$bx4i\n$bx4f\n"; my @p4f = URT::Person->get($bx4f); ok("@p4f", "flat query $bx4f works for " . scalar(@p4f) . " objects"); # we must flatten before query for this to work, and currently constant_values need support my @p4i = URT::Person->get($bx4i); ok("@p4i", "indirect query works"); is("@p4i", "@p4f", "indirect and flat query results match"); # the bx "operator" could be named "subquery" or we turn "matches" into "matches-bx" and "matches-regex" #my @p = URT::Person->get('primary_car.color' => 'red'); my $rule1 = URT::Car->define_boolexpr(color => 'red'); ok($rule1, "made a 'car has color red' rule"); note("$rule1"); #$DB::single = 1; my $rule2 = URT::Person->define_boolexpr('cars bx' => $rule1->id); ok($rule2, "made a 'person has primary_car with color is red'"); note("$rule2"); my @p = URT::Person->get($rule2); is(scalar(@p), 1, "got one person with a red primary car"); is($p[0]->id, 13, "got the expected person"); 94b_flatten_reframe.t100664023532023421 1205612544604517 17043 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1], is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); note("***** FLATTEN AND *****"); my $bx0 = URT::Person->define_boolexpr( 'is_cool' => 1, 'primary_car_color' => 'red', 'primary_car.engine.size' => 428, ); my $bx0f = $bx0->flatten(); my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'cars-primary_car.color' => 'red', 'cars-primary_car.engine.size' => 428, 'cars-primary_car?.is_primary true' => 1, ); is($bx0f->normalize->id, $bx1->normalize->id, "flattening works correctly"); note("***** REFRAME AND *****"); my $bx1r1 = $bx1->reframe('primary_car'); my $bx2 = URT::Car->define_boolexpr( 'owner.is_cool' => 1, 'color' => 'red', 'engine.size' => 428, 'is_primary true' => 1, ); is($bx1r1->normalize->id, $bx2->normalize->id, "reframe works for a one-step property embedding via/to/where"); my $bx1r2 = $bx1->reframe('primary_car.engine'); my $bx3 = URT::Car::Engine->define_boolexpr( 'car.owner.is_cool' => 1, 'car.color' => 'red', 'size' => 428, 'car.is_primary true' => 1, ); is($bx1r2->normalize->id, $bx3->normalize->id, "reframe works on a two-step chain with the first embedding via/to/where"); my $bx33 = URT::Person->define_boolexpr( 'primary_car.color' => 'red', 'is_cool true' => 1, ); my $bx33r = $bx33->reframe('primary_car'); my $bx33re = URT::Car->define_boolexpr( 'color' => 'red', 'owner.is_cool true' => 1, 'is_primary true' => 1, ); note("***** FLATTEN OR *****"); my $bx4 = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['primary_car.color' => 'red'], ] ); ok($bx4, "created an 'or' boolexpr"); my $bx4f = $bx4->flatten; ok($bx4f, "flattened an OR bx"); my $bx4fe = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['cars-primary_car.color' => 'red', 'cars-primary_car?.is_primary true' => 1], ] ); ok($bx4fe, "defined what we expect for a flattned OR rule"); is($bx4f->id, $bx4fe->id, "the flattened OR rule matches expectations"); note("***** REFRAME OR *****"); my $bx4r = $bx4->reframe('primary_car'); ok($bx4r, "reframed OR expression"); my $bx4re = URT::Car->define_boolexpr( -or => [ ['owner.is_cool' => 1], ['color' => 'red', 'is_primary true' => 1], ], ); ok($bx4re, "created expected reframe expression"); is($bx4r->id, $bx4re->id, "reframed expression matches the expected expression"); note("***** FLATTEN WITH ORDER/GROUP *****"); my $bx5 = URT::Person->define_boolexpr( 'is_cool true' => 1, 'primary_car_color' => 'red', '-group_by' => ['is_cool','primary_car_color','name'], '-order_by' => ['is_cool','primary_car_color'], ); my $bx5r = $bx5->reframe('primary_car'); my $bx5re = URT::Car->define_boolexpr( 'owner.is_cool true' => 1, 'color' => 'red', '-group_by' => ['owner.is_cool','color','owner.name'], '-order_by' => ['owner.is_cool','color'], 'is_primary true' => 1, ); is($bx5r->id, $bx5re->id, "reframe works on -order_by"); note("$bx5re\n$bx5r\n"); note("***** FLATTEN AROUND JOIN TO OPTIONAL WITH ON CLAUSE *****"); my $bx6 = URT::Person->define_boolexpr( is_cool => 1, -hints => ['primary_car'] ); my $bx6f = $bx6->flatten; 95_detect_db_deleted.t100664023532023421 2153712544604517 17153 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 49; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar, other varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?,?)'); foreach my $id ( 2..10 ) { $sth->execute($id,chr($id+64), chr($id+64)); # id 2 has balue B, id 3 has value C, etc } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value','other'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # A thing we've defined but does not exist in the DB my $defined_thing = URT::Thing->__define__(thing_id => '12345', value => 'CC', other => 'CC'); ok($defined_thing, 'Instantiate a URT::Thing with __define__'); # At the start, there are 9 items in the DB, plus the __define__d one my @things = URT::Thing->get(); is(scalar(@things), 10, 'Got all 10 things'); ok($dbh->do('delete from thing where thing_id = 4'), 'Delete thing_id 4 from the database'); # Deleted one, now there are 9 @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 9, 'reload() returned 9 things'); ok($dbh->do('delete from thing where thing_id = 6'), 'Delete thing_id 6 from the database'); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 8, 'get() returned 8 things'); # Change object 2's value from 'B' to ZZZ. Now there are 8 things in memory and the DB. # In the DB, object 2's value sorts first, but in memory it sorts last # Remaing object IDs should be 2,3,5,7,8,9,10,12345 my $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(2)->value('ZZZ'), "Change thing 2's value to ZZZ"); my @expected_ids = (3,12345,5,7,8,9,10,2); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 8, 'Got 8 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete object 2 from the DB. Reloading will throw an exception # because it's modified in memory, and modified in a conflicting manner in the # database (it's deleted) ok($dbh->do('delete from thing where thing_id = 2'), 'Delete thing_id 2 from database'); @things = eval { UR::Context->reload('URT::Thing', -order => ['value']) }; is(scalar(@things),0, 'Got no things back from reload()'); like($@, qr(URT::Thing ID '2' previously existed in an underlying), 'reload thew an exception about the deleted object'); # Undo the previous change so we won't get an exception again $trans->rollback; # After rolling back the transaction, Object ID 2 is still deleted from the database, # but exists in memory as an unchanged object. There are now 6 objects in the DB # and 8 in memory (don't forget the 1 that was __define_d # # Now, change object 10's value from J to A in memory. In the DB, object 10 sorts last, # but in memory it sorts first. $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(10)->value('A'), 'Change thing id 10 value to A'); @expected_ids = (10,3,12345,5,7,8,9); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 7, 'Got 7 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete it from the DB and make sure it throws an exception ok($dbh->do('delete from thing where thing_id = 10'), 'Delete thing_id 10 from database'); @things = eval { UR::Context->reload('URT::Thing', -order => ['value']) }; is(scalar(@things),0, 'Got no things back from reload()'); like($@, qr(URT::Thing ID '10' previously existed in an underlying), 'reload thew an exception about the deleted object'); $trans->rollback; # After the transaction, object ID 10 is still deleted from the database # but exists in memory as an unchanged object. There are now 5 objects in # the DB and 7 in memory (6 that came from the DB, plus the __define__d one) # Change object 3's value from C to ZZZ in the DB. In memory it will sort first, # but in the database it will sort last. $trans = UR::Context::Transaction->begin; ok($dbh->do("update thing set value = 'ZZZ' where thing_id = 3"), 'Change thing id 3 value to ZZZ in the database'); @expected_ids = (12345,5,7,8,9,3); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 6 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete the object ok(URT::Thing->get(3)->delete, 'Delete thing id 3 from memory'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 5, 'Got 4 object back from reload'); @expected_ids = (12345,5,7,8,9); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); $trans->rollback; # Change object 9's value from I to A in the DB. In memory it will sort # last, but in the DB it will sort first. Object 3 still has value 'ZZZ' $trans = UR::Context::Transaction->begin; ok($dbh->do("update thing set value = 'A' where thing_id = 9"), 'Change thing id 9 value to A in the database'); @expected_ids = (9,12345,5,7,8,3); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 6 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # now delete object ID 9 ok(URT::Thing->get(9)->delete, 'Delete thing id 9 from memory'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 5, 'Got 4 object back from reload'); @expected_ids = (12345,5,7,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Hack required in UR::Context::__merge... to get object 9 to correctly have # value => 'A' instead of 'I' $trans->rollback; # Try changing an unrelated property and do a query # # Object 9 is back again with value 'A' because of the rollback. $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(7)->other('blahblah'), 'Change thing id 7 "other" property'); ok($dbh->do("update thing set other = 'foofoo' where thing_id = 8"), 'Change thing id 8 "other" property in the database'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 4 objects back from reload'); @expected_ids = (9,12345,5,7,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); $trans->rollback; # Make a change to both an order-by and a filtered property in memory $trans = UR::Context::Transaction->begin; my $obj = URT::Thing->get(7); ok($obj->other('blahblah'), 'Change object 7s other property to blahblah'); ok($obj->value('A'), 'Change object 7s value to A'); @things = UR::Context->reload('URT::Thing', 'other ne' => 'blahblah', -order => ['value']); is(scalar(@things), 5, 'Got back 5 things from reload() where other is not blahblah'); @expected_ids = (9,12345,5,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); isa_ok($obj, 'URT::Thing', 'Thing id 7 was not deleted'); $trans->rollback; # Make a change to both an order-by and filtered property in the DB ok($dbh->do("update thing set other = 'blahblah' where thing_id = 7"), 'Change thing id 7 "other" property in the database'); ok($dbh->do("update thing set value = 'A' where thing_id = 7"), 'Change thing id 7 value to "A" in the database'); @things = UR::Context->reload('URT::Thing', 'other ne' => 'blahblah', -order => ['value']); is(scalar(@things), 5, 'Got back 5 things from reload() where other is not blahblah'); @expected_ids = (9,12345,5,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); ok(URT::Thing->get(7), 'Thing id 7 was not deleted'); ok($dbh->do('delete from thing'), 'Delete all remaining things from the database'); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 1, 'reload() returned one thing'); is($things[0]->id, $defined_thing->id, 'It was the thing we defined at the beginning of the test'); ok($defined_thing->delete, "Delete the defined object"); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 0, 'reload() returned no objects'); 95_normalize_property_description.t100664023532023421 570312544604517 22074 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use above 'UR'; use Test::More; my @desc = ( class_name => 'Foo', has_abstract_constant => [ subject_class_name => { is_abstract => 1, is_constant => 1 }, perspective => { is_abstract => 1, is_constant => 1 }, toolkit => { is_abstract => 1, is_constant => 1 }, ], has_optional => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => 'when nested inside another view, this references that view', }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being observed' }, aspects => { is => 'UR::Object::View::Aspect', reverse_as => 'parent_view', is_many => 1, specify_by => 'name', order_by => 'number', doc => 'the aspects of the subject this view renders' }, default_aspects => { is => 'ARRAY', is_abstract => 1, is_constant => 1, is_many => 1, # technically this is one "ARRAY" default_value => undef, doc => 'a tree of default aspect descriptions' }, ], has_optional_transient => [ _widget => { doc => 'the object native to the specified toolkit which does the actual visualization' }, _observer_data => { is => 'HASH', is_transient => 1, value => undef, # hashref set at construction time doc => ' hooks around the subject which monitor it for changes' }, ], has_many_optional => [ aspect_names => { via => 'aspects', to => 'name' }, ] ); my $class_name = "UR::Object::View"; my $new_desc = UR::Object::Type->_normalize_class_description(@desc); ok($new_desc, 'normalized class object'); my $new_desc2 = UR::Object::Type->_normalize_class_description(%$new_desc); ok($new_desc2, 'normalized class object again'); is_deeply($new_desc, $new_desc2, '2x normalization produces consistent answer') or diag Data::Dumper::Dumper($new_desc, $new_desc2); # Test that an illegal property name throws an exception foreach my $property_name ( ('has a space', 'HASH=(0x1234)', 'has.a.dot', '/path/name', '$var_name') ) { my $new_desc = eval { UR::Object::Type->_normalize_class_description( class_name => 'Foo', has => [ $property_name => { is => 'Number' }, ], ) }; my $escaped_property_name = quotemeta($property_name); like($@, qr/Invalid property name in class Foo: '$escaped_property_name' /, "Got exception for invalid property name '$property_name'"); is($new_desc, undef, '_normalize_class_description() returns undef'); } done_testing(); 95b_subclass_description_preprocessor_errors.t100664023532023421 147412544604517 24314 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use above 'UR'; use Test::More; class Base { is => 'UR::Object', subclass_description_preprocessor => 'Base::_preprocess', subclassify_by => 'subclass_name', }; package Base; sub _preprocess { my ($class, $desc) = @_; my $count_prop = $desc->{has}{count}; $desc->{has}{extra_property} = { is => 'Number', data_type => 'Number', property_name => 'extra_property', class_name => $count_prop->{class_name}, }; return $desc; } package main; eval { class Derived { is => 'Base', has => [ count => { is => 'Number', }, ], }; }; ok($@, "specifying redundant/ambiguous properties via preprocessing is an error"); done_testing(); 95c_detect_changed_in_memory_filter.t100664023532023421 252112544604517 22227 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar, other varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?,?)'); foreach my $id ( 2..10 ) { $sth->execute($id, chr($id + 64), chr($id + 64)) } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value','other'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # Changing an object in memory and filtering on that change should not register as a DB deletion. # - Had to get with multiple IDs in order to reproduce the bug we found. # - Had to separate this in its own test because 95_detect_db_deleted.t's environment was unable to reproduce the bug. # - The bug was that UR::Context::LoadingIterator was treating the case where a BoolExpr filter change was causing an exception to be thrown. my @ids = (3, 5, 7); my @things = URT::Thing->get(id => \@ids); map { $_->value('A') } @things; my @same_things = URT::Thing->get(value => 'A', id => \@ids); is(scalar @things, scalar @same_things, 'got same number of same things as we created A'); done_testing(); 96_context_clear_cache.t100664023532023421 416712544604517 17506 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 19; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?)'); foreach my $id ( 1..5 ) { $sth->execute($id,$id); } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); my $query_count = 0; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); $query_count = 0; my @things = URT::Thing->get(); is(scalar(@things), 5, 'Got all 5 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->is_loaded(); is(scalar(@things), 5, 'is_loaded returns all 5 things'); is($query_count, 0, 'Made no queries'); UR::Context->dump_warning_messages(0); ok(UR::Context->current->clear_cache(), 'clear cache'); $query_count = 0; @things = URT::Thing->is_loaded(); is(scalar(@things), 0, 'is_loaded now shows no things in memory'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 5, 'Got all 5 things'); is($query_count, 1, 'Made 1 query'); ok(UR::Context->current->clear_cache(), 'clear cache'); $query_count = 0; @things = URT::Thing->get('value <' => 3); is(scalar(@things), 2, 'Got 2 things with value < 3'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get('value >' => 3); is(scalar(@things), 2, 'Got 2 things with value > 3'); is($query_count, 1, 'Made 1 query'); ok(UR::Context->current->clear_cache(), 'clear cache'); my @things2 = URT::Thing->is_loaded(); is(scalar(@things2), 0, 'Still saw 0 things in memory'); #print Data::Dumper::Dumper(\@things); is(scalar(@things), 2, '2 objects are still held in the list'); isa_ok($_, 'UR::DeletedRef') foreach @things; 96b_ur_context_class_commit_triggers_observer.t100664023532023421 166212544604517 24434 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; __PACKAGE__->main('UR'); sub main { my ($test, $module) = @_; use_ok($module) or exit; $test->ur_context_class_commit_triggers_observer; done_testing(); } sub ur_context_class_commit_triggers_observer { my $self = shift; my $context = UR::Context->current; my @expected_signals = ( [ $context, 'precommit' ], [ $context, 'sync_databases' => 1 ], [ $context, 'commit' => 1 ], ); my @signals_fired; foreach my $signal ( @expected_signals ) { my $aspect = $signal->[1]; $context->add_observer( aspect => $aspect, callback => sub { push @signals_fired, [ @_ ]; } ); } ok(UR::Context->commit, 'UR::Context committed'); is_deeply(\@signals_fired, \@expected_signals, 'Got expected signals and args'); } 96c_ur_context_current_and_process.t100664023532023421 134612544604517 22204 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; __PACKAGE__->main('UR'); sub main { my ($test, $module) = @_; use_ok($module) or exit; $test->ur_context_process_is_distinguished_from_current; done_testing(); } sub ur_context_process_is_distinguished_from_current { my $self = shift; my $cc = UR::Context->current; my $cp = UR::Context->process; is($cc->id, $cp->id, 'current returned the same as process'); my $tx = UR::Context::Transaction->begin; my $new_cc = UR::Context->current; my $new_cp = UR::Context->process; isnt($new_cc->id, $cc->id, 'current changed within transaction'); is ($new_cp->id, $cp->id, 'process did not change within transaction'); } 97_used_libs.t100664023532023421 356612544604517 15505 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More; require UR::Util; require Cwd; require File::Temp; { local @INC = ('/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, []), 'no used_libs'); } { local @INC = ('/foo'); local $ENV{PERL5LIB} = ''; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'empty PERL5LIB'); } { local @INC = ('/foo', '/bar', '/baz'); local $ENV{PERL5LIB} = '/bar:/baz'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'multiple dirs in PERL5LIB'); } { local @INC = ('/foo', '/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'only one item in PERL5LIB (no trailing colon)'); } { local @INC = ('/foo', '/bar', '/baz'); local $ENV{PERL5LIB} = '/bar/:/baz'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'first dir in PERL5LIB ends with slash (@INC may not have slash)'); } { local @INC = ('/foo', '/foo', '/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'remove duplicate elements from used_libs'); } { local @INC = ('/foo'); local $ENV{PERL5LIB} = ''; local $ENV{PERL_USED_ABOVE} = '/foo/'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'remove trailing slash from used_libs'); } { my $orig_dir = Cwd::cwd(); my $temp_dir = File::Temp::tempdir(CLEANUP => 1); $DB::single = 1; my @pre_chdir_used_libs = UR::Util::used_libs(); chdir($temp_dir); $DB::single = 1; my @post_chdir_used_libs = UR::Util::used_libs(); chdir($orig_dir); is_deeply(\@pre_chdir_used_libs, \@post_chdir_used_libs, 'used_libs returns same libs after chdir'); } done_testing(); 98_ur_update.t100775023532023421 7550412544604517 15551 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; #BEGIN { $ENV{UR_CONTEXT_BASE} = "URT::Context::Testing" }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use DBI; use IO::Pipe; use Test::More; use File::Temp; use File::Copy; if ($^O eq 'darwin') { plan skip_all => 'known to fail OS X' } elsif ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'skip running during install', } else { plan tests => 87; } use UR::Namespace::Command::Update::ClassesFromDb; UR::DBI->no_commit(1); # This can only be run with the cwd at the top of the URT namespace require Cwd; my $namespace_dir = URT->get_base_directory_name; my $working_dir = Cwd::abs_path(); if ($working_dir ne $namespace_dir) { if (-d $namespace_dir) { chdir($namespace_dir); } else { die "Cannot determine URT's namespace directory, exiting"; } } cleanup_files(); sub cleanup_files { #unlink $sqlite_file; #$DB::single = 1; my $namespace_dir = URT->get_base_directory_name; for my $filename ( qw| Car.pm Employee.pm Person.pm .deleted/Car.pm .deleted/Employee.pm .deleted/Person.pm | ) { if (-e "$namespace_dir/$filename") { #warn "unlinking $filename\n"; unlink "$namespace_dir/$filename"; } } } UR::Namespace::Command::Update::ClassesFromDb->dump_error_messages(1); UR::Namespace::Command::Update::ClassesFromDb->dump_warning_messages(1); UR::Namespace::Command::Update::ClassesFromDb->dump_status_messages(0); UR::Namespace::Command::Update::ClassesFromDb->status_messages_callback( sub { my $self = shift; my $msg = shift; print " $msg\n"; return 1; } ); # We launch a similar command multiple times. my($delegate_class,$create_params) = UR::Namespace::Command::Update::ClassesFromDb->resolve_class_and_params_for_argv(qw(--data-source URT::DataSource::SomeSQLite)); ok($delegate_class, "Resolving parameters for update: class is $delegate_class"); my $command_obj = sub { my $command_obj = $delegate_class->create(%$create_params, _override_no_commit_for_filesystem_items => 1); ok($command_obj, "created new command for " . join(" ", @_)); ok($command_obj->execute(), "executed new command for " . join(" ",@_)); return $command_obj->result; }; bless ($command_obj,"DummyExecutor"); sub DummyExecutor::execute { shift->(@_); } ok($command_obj, "Created a dummy command object for updating the classes"); my $ds_class = 'URT::DataSource::SomeSQLite'; # The datasource we'll be making tables in $ds_class->class; set_data_dump_path_to_tmp('URT::DataSource::Meta'); my $dbh = $ds_class->get_default_handle(); ok($dbh, 'Got database handle'); # This wrapper to get_changes filters out things like the command-line parameters # until that bug is fixed. my $trans; sub get_changes { my @changes = grep { $_->changed_class_name ne 'UR::Namespace::Command::Update::ClassesFromDb' } grep { $_->changed_class_name ne "UR::Namespace::CommandParam" } grep { $_->changed_class_name ne 'UR::DataSource::Meta' && substr($_->changed_aspect,0,1) ne '_'} grep { $_->changed_aspect ne 'query' } $trans->get_change_summary(); return @changes; } sub cached_dd_objects { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); } sub cached_dd_object_count { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); return scalar(@obj); } sub cached_class_object_count { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /UR::Object::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); return scalar(@obj); } sub cached_person_dd_objects { my $cx = UR::Context->current; my @obj = grep { $_->{table_name} eq "person" } grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); } sub cached_person_summary { my @obj = map { ref($_) . "\t" . $_->{id} } cached_person_dd_objects(); return @obj; } sub undo_log_summary { my @c = do { no warnings; reverse @UR::Context::Transaction::change_log; }; return map { $_->{changed_class_name} . "\t" . $_->{changed_id} . "\t" . $_->{changed_aspect} } grep { not ($_->{changed_class_name} =~ /^UR::Object/ and $_->{changed_aspect} eq "load") } @c; } # Hack - These get filled in at the bottom initialize_check_changes_data_structure() our($check_changes_1, $check_changes_2, $check_changes_3); # Empty schema $trans = UR::Context::Transaction->begin(); ok($trans, "began transaction"); ok($command_obj->execute(),'Executing update on an empty schema'); my @changes = get_changes(); is(scalar(@changes),0, "zero changes for an empty schema"); # note this for comparison in future tests. my $expected_dd_object_count = cached_dd_object_count(); # don't rollback # Make a table ok($dbh->do('CREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, name varchar)'), 'Create person table'); $trans = UR::Context::Transaction->begin(); ok($trans, "CREATED PERSON and began transaction"); ok($command_obj->execute(),'Executing update after creating person table'); initialize_check_change_data_structures(); @changes = get_changes(); # FIXME The test should probably break out each type of changed thing and check # that the counts of each type are correct, and not just the count of all changes my $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_1, "Change list is correct"); my $personclass = UR::Object::Type->get('URT::Person'); isa_ok($personclass, 'UR::Object::Type'); # FIXME why isn't this a UR::Object::Type ok($personclass->module_source_lines, 'Person class module has at least one line'); is($personclass->class_name, 'URT::Person', 'Person class class_name is correct'); is($personclass->table_name, 'main.person', 'Person class table_name is correct'); is($UR::Context::current->resolve_data_sources_for_class_meta_and_rule($personclass)->id, $ds_class, 'Person class data_source is correct'); is_deeply([sort $personclass->direct_column_names], ['name','person_id'], 'Person object has all the right columns'); is_deeply([$personclass->direct_id_column_names], ['person_id'], 'Person object has all the right id column names'); # Another test case should make sure the other class introspection methods like inherited_property_names, # all_table_names, etc work correctly for all kinds of objects my $module_path = $personclass->module_path; ok($module_path, "got a module path"); ok(-f $module_path, 'Person.pm module exists'); ok(! UR::Object::Type->get('URT::NonExistantClass'), 'Correctly cannot load a non-existant class'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Make the employee and car tables refer to person, and add a column to person ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY CONSTRAINT fk_person_id REFERENCES person(person_id), rank integer)'), 'Employee inherits from Person'); ok($dbh->do('ALTER TABLE person ADD COLUMN postal_address varchar'), 'Add column to Person'); ok($dbh->do('CREATE TABLE car (car_id integer NOT NULL PRIMARY KEY, owner_id integer NOT NULL CONSTRAINT fk_person_id2 REFERENCES person(person_id), make varchar, model varchar, color varchar, cost number)'), 'Create car table'); $trans = UR::Context::Transaction->begin(); ok($trans, "CREATED EMPLOYEE AND CAR AND UPDATED PERSON and began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_2, "Change list is correct"); # Verify the Person.pm and Employee.pm modules exist $personclass = UR::Object::Type->get('URT::Person'); ok($personclass, 'Person class loaded'); my %got = map { $_ => 1} $personclass->direct_column_names; is_deeply(\%got, { name => 1, person_id => 1, postal_address => 1 }, 'Person object has all the right columns'); %got = map { $_ => 1 } $personclass->all_property_names; is_deeply(\%got, { name => 1, person_id => 1, postal_address => 1 }, 'Person object has all the right properties'); %got = map { $_ => 1 } $personclass->direct_id_column_names; is_deeply(\%got, { person_id => 1 }, 'Person object has all the right id column names'); my $employeeclass = UR::Object::Type->get('URT::Employee'); ok($employeeclass, 'Employee class loaded'); isa_ok($employeeclass, 'UR::Object::Type'); # There is no standardized way to spot inheritance from the schema. # The developer can reclassify in the source, and subsequent updates would respect it. # FIXME: test for this. # What about if one class' primary keys are all foreign keys to all of another class' primary keys? # FIXME - what about foreign keys not involving primary keys? Make object accessor properties ok(! $employeeclass->isa('URT::Car'), 'Employee class is correctly not a Car'); ok($employeeclass->module_source_lines, 'Employee class module has at least one line'); %got = map { $_ => 1 } $employeeclass->direct_column_names; is_deeply(\%got, { employee_id => 1, rank => 1 }, 'Employee object has all the right columns'); %got = map { $_ => 1 } $employeeclass->all_property_names; is_deeply(\%got, { employee_id => 1, person_employee => 1, rank => 1 }, 'Employee object has all the right properties'); %got = map { $_ => 1 } $employeeclass->direct_id_column_names; is_deeply(\%got, { employee_id => 1 }, 'Employee object has all the right id column names'); ok($employeeclass->table_name eq 'main.employee', 'URT::Employee object comes from the employee table'); my $carclass = UR::Object::Type->get('URT::Car'); ok($carclass, 'Car class loaded'); is($carclass->class_name,'URT::Car', "class name is set correctly"); isa_ok($carclass,'UR::Object::Type'); ok(! $carclass->class_name->isa('URT::Person'), 'Car class is correctly not a Person'); %got = map { $_ => 1 } $carclass->direct_column_names; is_deeply(\%got, { car_id => 1, color => 1, cost => 1, make => 1, model => 1, owner_id => 1 }, 'Car object has all the right columns'); %got = map { $_ => 1 } $carclass->all_property_names; is_deeply(\%got, { car_id => 1, color => 1, cost => 1, make => 1, model => 1, owner_id => 1, person_owner => 1 }, 'Car object has all the right properties'); %got = map { $_ => 1 } $carclass->direct_id_column_names; is_deeply(\%got, { car_id => 1 }, 'Car object has all the right id column names'); ok($carclass->table_name eq 'main.car', 'Car object comes from the car table'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Drop a table ok($dbh->do('DROP TABLE car'),'Removed Car table'); $trans = UR::Context::Transaction->begin(); ok($trans, "DROPPED CAR and began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_3, "Change list is correct"); ok($personclass = UR::Object::Type->get('URT::Person'),'Loaded Person class'); ok($employeeclass = UR::Object::Type->get('URT::Employee'), 'Loaded Employee class'); $carclass = UR::Object::Type->get('URT::Car'); ok(!$carclass, 'Car class is correctly not loaded'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Drop a constraint # SQLite doesn't support altering a table to drop a constraint, so we need to # drop the table and recreate it without the constraint #ok($dbh->do('DROP TABLE employee'), 'Temporarily dropping table employee'); #ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY, rank integer)'), 'Recreate Employee without constraint'); #$trans = UR::Context::Transaction->begin(); #ok($trans, "Changed EMPLOYEE and began transaction"); # # ok($command_obj->execute(), 'Updating schema'); # my @o = UR::DataSource::RDBMS::FkConstraint->get(namespace => 'URT'); #print "\n\n*** Got ",scalar(@o)," FkConstraints\n"; # @changes = get_changes(); # $changes_as_hash = convert_change_list_for_checking(@changes); # # Drop the other two tables ok($dbh->do('DROP TABLE employee'),'Removed employee table'); ok($dbh->do('DROP TABLE person'),'Removed person table'); ok($dbh->do('CREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, postal_address varchar)'), 'Replaced table person w/o column "name".'); #ok($dbh->do('ALTER TABLE person DROP column name'),'Removed the name column from the person table'); ##begin(); ok($trans, "DROPPED EMPLOYEE AND UPDATED PERSON began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); is(scalar(@changes), 15, "found changes for two more dropped tables"); $trans = UR::Context::Transaction->begin(); ok($trans, "Restarted transaction since some data is not really sync'd at sync_filesystem"); ok($command_obj->execute(), 'Updating schema anew.'); ok(! UR::Object::Type->get('URT::Employee'), 'Correctly could not load Employee class'); ok(! UR::Object::Type->get('URT::Car'),'Correctly could not load Car class'); $personclass = UR::Object::Type->get('URT::Person'); unless ($personclass) { #$DB::single = 1; } $personclass->ungenerate; #$DB::single = 1; $personclass->generate; ok($personclass, 'Person class loaded'); is_deeply([sort $personclass->direct_column_names], ['person_id','postal_address'], 'Person object has all the right columns'); is_deeply([sort $personclass->class_name->__meta__->all_property_names], ['person_id','postal_address'], 'Person object has all the right properties'); is_deeply([$personclass->direct_id_column_names], ['person_id'], 'Person object has all the right id column names'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Clean up after now-defunct class module files and SQLIte DB file cleanup_files(); sub child_db_interaction { my $dbfile = shift; my $pid; my $result = IO::Pipe->new(); my $to_child = IO::Pipe->new(); if ($pid = fork()) { $to_child->writer; $to_child->autoflush(1); $result->reader(); my @commands = map {$_ . "\n"} @_; foreach my $cmd ( @commands ) { $to_child->print($cmd); my $result = $result->getline(); chomp($result); my($retval,$string,$dbierr) = split(';',$result); return undef unless $retval; } $to_child->print("exit\n"); waitpid($pid,0); return 1; } else { $to_child->reader(); $result->writer(); $result->autoflush(1); my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","",""); unless ($dbh) { $result->print("0;can't connect;$DBI::errstr\n"); exit(1); } while(my $sql = $to_child->getline()) { chomp($sql); last if ($sql eq 'exit' || !$sql); my $sth = $dbh->prepare($sql); unless ($sth) { $result->print("0;prepare failed;$DBI::errstr\n"); $result->print("0;prepare failed;$DBI::errstr\n"); next; } my $retval = $sth->execute(); if ($retval) { $result->print($retval . "\n"); } else { $result->print("0;execute failed;$DBI::errstr\n"); } } $dbh->commit(); exit(0); } # end child } # Convert the list of changes to a data structure matching the expected changes sub convert_change_list_for_checking { my(@changes_list) = @_; my $changes = {}; foreach my $change ( @changes_list ) { my $changed_class_name = $change->{'changed_class_name'}; my $changed_id = $change->{'changed_id'}; my $changed_aspect = $change->{'changed_aspect'}; next if $changed_aspect eq 'query'; my $undo_data = $change->{'undo_data'}; if (exists $changes->{$changed_class_name}->{$changed_id}->{$changed_aspect}) { die "Two types of changes for the same thing in the same transaction!?"; } $changes->{$changed_class_name}->{$changed_id}->{$changed_aspect} = defined($undo_data); } return $changes; } # These expected change data structures work like this: # Based on the UR::Change objects, the first level hash key is # the changed_class_name, second level key is the changed_id, # the third level key is the changed_aspect, and the final value # is whether the undo_data is defined or not. # # Note that because of the way this testcase works, by creating a # transaction, updating metadata, and the rolling back the transaction, # all these metadata objects are always "created" as new, and not # modifications of existing things. # # There should probably be tests for the usual case where you would be # updating existing classes based on DB changes # Changes after creating the person table and running ur update classes sub initialize_check_change_data_structures { my $sqlite_owner = 'main'; $check_changes_1 = { 'UR::DataSource::RDBMS::Table' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.person" => { create => '', # Meta DB object is created for the person table er_type => '', # And ur update classes fills in an er_type }, }, 'UR::DataSource::RDBMS::TableColumn' => { # The (new) person table has 2 (new) columns, person_id and name "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id" => { create => '' }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tname" => { create => '' }, }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { # person_id is the first and only primary column constraint "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id\t1" => { create => '' }, }, 'UR::Object::Type' => { # A new metaclass gets created for the person table 'URT::Person::Type' => { create => '' }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', # And then the class that goes with the table rewrite_module_header => 1, # And a record that we wrote a perl module on the filesystem }, }, 'UR::Object::Property' => { # Two new properties for the person class, name and person_id "URT::Person\tname" => { create => '' }, "URT::Person\tperson_id" => { create => '', is_id => '', # Created as an ID property }, }, }; # Changes after creating the car and employee tables, and adding postal_address column to person $check_changes_2 = { 'UR::DataSource::RDBMS::Table' => { # 3 tables: person, employee and car "URT::DataSource::SomeSQLite\t${sqlite_owner}.person" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car" => { create => '', er_type => '', }, }, 'UR::DataSource::RDBMS::TableColumn' => { # Table person now has 3 columns: person_id, name and postal_address "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tname" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tpostal_address" => { create => '', }, # table employee has 2 columns: employee_id and rank "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\trank" => { create => '', }, # table car has these columns: car_id, make, model, color and cost "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tcar_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tmake" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tmodel" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tcolor" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tcost" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\towner_id" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraint' => { # Both employee and car tables have foreign keys to person "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\t${sqlite_owner}.person\tfk_person_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\t${sqlite_owner}.person\tfk_person_id2" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraintColumn' => { # The employee table FK points from employee_id to person_id "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\tfk_person_id\temployee_id" => { create => '', }, # The car table FK points from owner_id to person_id "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tfk_person_id2\towner_id" => { create => '', } }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { # All three tables have PK constraints for their ID columns "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.car\tcar_id\t1" => { create => '', }, }, # running ur update classes makes 3 new classes 'UR::Object::Type' => { "URT::Car::Type" => { create => '', }, "URT::Employee::Type" => { create => '', }, "URT::Person::Type" => { create => '', }, }, # Each class has a property for the respective tablecolumn 'UR::Object::Property' => { "URT::Car\tcar_id" => { create => '', is_id => '', }, "URT::Car\tcolor" => { create => '', }, "URT::Car\tcost" => { create => '', }, "URT::Car\tmake" => { create => '', }, "URT::Car\tmodel" => { create => '', }, "URT::Car\towner_id" => { create => '', }, "URT::Car\tperson_owner" => { create => '', }, "URT::Employee\temployee_id" => { create => '', is_id => '', }, "URT::Employee\trank" => { create => '', }, "URT::Employee\tperson_employee" => { create => '', }, "URT::Person\tname" => { create => '', }, "URT::Person\tperson_id" => { create => '', is_id => '', }, "URT::Person\tpostal_address" => { create => '', }, }, # There a record of creating an instance of each class, and # that we wrote a perl module on the filesystem 'URT::Car::Type' => { 'URT::Car' => { create => '', rewrite_module_header => 1, }, }, 'URT::Employee::Type' => { 'URT::Employee' => { create => '', rewrite_module_header => 1, }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', rewrite_module_header => 1, }, }, # Because we rolled back the previous transaction, the old metadata # objects became ghosts. This is suboptimal and makes little sense # but there it is... 'UR::DataSource::RDBMS::Table::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.person" => { delete => 1, }, }, 'UR::DataSource::RDBMS::TableColumn::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tname" => { delete => 1, }, }, 'UR::DataSource::RDBMS::PkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id\t1" => { delete => 1, }, }, }; # After removing the car table $check_changes_3 = { # FIXME Why are there no ghost objects for the dropped car stuff? 'UR::DataSource::RDBMS::Table' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person" => { create => '', er_type => '', }, }, 'UR::DataSource::RDBMS::TableColumn' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\trank" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tname" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tpostal_address" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraint' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\t${sqlite_owner}.person\tfk_person_id" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraintColumn' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\tfk_person_id\temployee_id" => { create => '', }, }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id\t1" => { create => '', }, }, 'URT::Employee::Type' => { 'URT::Employee' => { create => '', rewrite_module_header => 1, }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', rewrite_module_header => 1, }, }, 'UR::Object::Type' => { 'URT::Person::Type' => { create => '', }, 'URT::Employee::Type' => { create => '', }, }, 'UR::Object::Property' => { "URT::Employee\temployee_id" => { create => '', is_id => '', }, "URT::Employee\trank" => { create => '', }, "URT::Employee\tperson_employee" => { create => '', }, "URT::Person\tperson_id" => { create => '', is_id => '', }, "URT::Person\tname" => { create => '', }, "URT::Person\tpostal_address" => { create => '', }, }, 'UR::DataSource::RDBMS::Table::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.person" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee" => { delete => 1, }, }, 'UR::DataSource::RDBMS::TableColumn::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\trank" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tname" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tpostal_address" => { delete => 1, }, }, 'UR::DataSource::RDBMS::FkConstraint::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\t${sqlite_owner}.person\tfk_person_id" => { delete => 1, }, }, 'UR::DataSource::RDBMS::FkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\tfk_person_id\temployee_id" => { delete => 1, }, }, 'UR::DataSource::RDBMS::PkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t${sqlite_owner}.employee\temployee_id\t1" => { delete => 1, }, "URT::DataSource::SomeSQLite\t${sqlite_owner}.person\tperson_id\t1" => { delete => 1, }, }, }; } sub set_data_dump_path_to_tmp { my $ds_class = shift; my $ds = $ds_class->_singleton_object(); my $tmpfh = File::Temp->new( UNLINK => 1, TEMPLATE => 'ur_update_classes_testXXXXXX'); my $orig_data_dump_path = $ds->_data_dump_path; if (-f $orig_data_dump_path) { File::Copy::copy($orig_data_dump_path, $tmpfh->filename); } my $sub = sub { $tmpfh->filename }; Sub::Install::reinstall_sub({ code => $sub, as => '_data_dump_path', into => $ds_class, }); } 1; 99-autounload-pool.t100664023532023421 644512544604517 16575 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests=> 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; setup_classes(); subtest 'normal operation' => sub { plan tests => 7; my $kept = URT::Thing->get(1); my $also_kept = URT::Thing->get(related_number => 2); do { my $unloader = UR::Context::AutoUnloadPool->create(); URT::Thing->get(98); URT::Thing->get(related_number => 2); # re-get something gotten in the outer scope URT::Thing->get(related_number => 99); }; ok(URT::Thing->is_loaded(id => $kept->id), 'URT::Thing is still loaded'); ok(URT::Thing->is_loaded(id => $also_kept->id), 'other URT::Thing is still loaded'); ok(URT::Related->is_loaded(id => $also_kept->id), 'URT::Related is still loaded'); foreach my $id ( 98, 99 ) { ok(! URT::Thing->is_loaded(id => $id), "Expected URT::Thing $id was unloaded"); ok(! URT::Related->is_loaded(id => $id), "Expected URT::Related $id was unloaded"); } }; subtest 'exception while unloading' => sub { plan tests => 2; my $changed_id = 99; my $unchanged_id = 98; do { my $unloader = UR::Context::AutoUnloadPool->create(); my $thing = URT::Thing->get($changed_id); $thing->changable_prop(1000); URT::Thing->get($unchanged_id); }; ok(URT::Thing->is_loaded($changed_id), 'Changed object did not get unloaded'); ok(! URT::Thing->is_loaded($unchanged_id), 'Unchanged object did get unloaded'); }; subtest 'call delete on pool' => sub { plan tests => 2; my $kept_id = 100; do { my $unloader = UR::Context::AutoUnloadPool->create(); URT::Thing->get($kept_id); ok($unloader->delete, 'Delete the auto unloader'); }; ok(URT::Thing->is_loaded($kept_id), 'Object was not unloaded'); }; subtest 'does not unload meta objects' => sub { plan tests => 3; ok(! UR::Object::Type->is_loaded('URT::Thingy'), 'URT::Thingy is not loaded yet'); do { my $unloader = UR::Context::AutoUnloadPool->create(); URT::Thingy->class; # Load a class in the URT namespace }; ok(UR::Object::Type->is_loaded('URT::Thingy'), 'Class object is still loaded'); ok(UR::Object::Property->is_loaded(class_name => 'URT::Thingy', property_name => 'enz_id'), "Class' property object is still loaded"); }; sub setup_classes { my $generic_loader = sub { my($class_name, $rule, $expected_headers) = @_; my $value; foreach my $prop ( $rule->template->_property_names ) { if ($value = $rule->value_for($prop)) { last; } } my @value = ($value) x scalar(@$expected_headers); return ($expected_headers, [ \@value ]); }; class URT::Related { id_by => 'id', data_source => 'UR::DataSource::Default', }; *URT::Related::__load__ = $generic_loader; class URT::Thing { id_by => 'id', has => [ changable_prop => { is => 'Number' }, related => { is => 'URT::Related', id_by => 'id' }, related_number => { via => 'related', to => 'id' }, ], data_source => 'UR::DataSource::Default', }; *URT::Thing::__load__ = $generic_loader; } 99-transaction-unload-defined-objects.t100664023532023421 560712544604517 22302 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; use URT::DataSource::SomeSQLite; # There was a bug where after a software transaction rollback, a previously # run query would die because the joins used by a query had been unloaded by # the rollback(). Since UR::Object::Join has its own caching, the unloaded # joins hung around as DeletedRefs. Those later queries would re-use these # cached lists of (now unloaded/deleted) joins and crash in QueryPlan # # The fix was to override unload() in UR::Object::Join my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table owner (owner_id integer PRIMARY KEY, name varchar)"), 'Created owner table'); ok( $dbh->do("create table thing (thing_id integer PRIMARY KEY, name varchar, owner_id integer REFERENCES owner(owner_id))"), 'Created thing table'); ok( $dbh->do("insert into owner values (1, 'Bob')"), 'Insert owner Bob'); ok( $dbh->do("insert into thing values (1, 'car', 1)"), "insert Bob's car"); ok( $dbh->do("insert into thing values (2, 'truck', 1)"), "insert Bob's truck"); ok( $dbh->do("insert into thing values (3, 'boat', 1)"), "insert Bob's boat"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Owner', id_by => ['owner_id'], has => [ name => { is => 'String' }, things => { is => 'URT::Thing', reverse_as => 'owner', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'owner', ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, owner => { is => 'URT::Owner', id_by => 'owner_id' }, owner_name => { via => 'owner', to => 'name' }, owner_name2 => { via => 'owner', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); my $t = UR::Context::Transaction->begin(); ok($t, 'Start transaction'); my $meta = URT::Thing->__meta__; ok($meta, 'Class object for URT::Thing'); my $prop = $meta->property_meta_for_name('owner_name'); ok($prop, 'Property meta for owner_name'); my @joins = $prop->_resolve_join_chain(); foreach (@joins) { isa_ok($_, 'UR::Object'); } ok($t->rollback, 'Rollback'); @joins = $prop->_resolve_join_chain(); foreach (@joins) { isa_ok($_, 'UR::Object'); } # Another related problem is that UR::DataSource::Default objects were # getting unloaded $t = UR::Context::Transaction->begin(); ok($t, 'Start another transaction'); my @things = URT::Thing->get(owner_name => 'Bob'); ok(scalar(@things), "Get Bob's things"); ok($t->rollback(), 'Rollback'); @things = URT::Thing->get(owner_name2 => 'Bob'); ok(scalar(@things), "Get Bob's again"); 99_transaction-failed_commit_rollback.t100664023532023421 362612544604517 22523 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use UR; use IO::File; use Test::More tests => 11; UR::Object::Type->define( class_name => 'Circle', has => [ radius => { is => 'Number', default_value => 1, }, ], ); # Create a Circle my $circle = Circle->create(); ok($circle->isa('Circle'), 'create a circle'); ok($circle->radius == 1, 'default radius is 1'); { my $transaction = UR::Context::Transaction->begin; isa_ok($transaction, 'UR::Context::Transaction'); my $old_radius = $circle->radius; my $new_radius = $circle->radius + 5; isnt($circle->radius, $new_radius, "new circle radius isn't current radius"); $circle->radius($new_radius); is($circle->radius, $new_radius, "circle radius changed to new radius"); *Circle::__errors__ = sub { my $tag = UR::Object::Tag->create ( type => 'invalid', properties => ['test_property'], desc => 'intentional error for test', ); return ($tag); }; $transaction->dump_error_messages(0); $transaction->queue_error_messages(1); is($transaction->commit, undef, 'commit failed'); my @messages = $transaction->error_messages(); is(scalar(@messages), 2, 'commit generated 2 error messages'); my $circleid = $circle->id; is($messages[0], 'Invalid data for save!', 'First error text is correct'); #like($msgobj[1]->text, # qr(Circle identified by $circleid has problem on\nINVALID: property 'test_property': intentional error for test)m, # 'Error message text is correct'); like($messages[1], qr(Circle identified by $circleid has problems on\s+INVALID: property 'test_property': intentional error for test), 'Error message text is correct'); is($transaction->rollback, 1, 'rollback succeeded'); is($circle->radius, $old_radius, 'circle radius was rolled back due to forced __errors__'); } 1; 99_transaction-observers.t100664023532023421 1767212544604517 20116 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t use strict; use warnings; use UR; use IO::File; use Test::More tests => 57; UR::Object::Type->define( class_name => 'Circle', has => [ radius => { is => 'Number', default_value => 1, }, ], ); sub add_test_observer { my ($aspect, $context, $observer_ran_ref) = @_; $$observer_ran_ref = 0; my $observer; my $callback; $callback = sub { $$observer_ran_ref = 1; }; $observer = $context->add_observer( aspect => $aspect, callback => $callback, ); unless ($observer) { die "Failed to add $aspect observer!"; } return $observer; } # Create a Circle my $circle = Circle->create(); ok($circle->isa('Circle'), 'create a circle'); ok($circle->radius == 1, 'default radius is 1'); # Verify Transaction Rollback Removes Observer and its Subscription # making sure if someone tries to catch their observer's delete that it runs # before the observer's self-created delete subscription { my $ran_observer_observer = 0; my $circle_trans = UR::Context::Transaction->begin(); ok($circle_trans, 'begin transaction'); my $ran_circle_radius_observer = 0; my $circle_obs = $circle->add_observer( aspect => 'radius', callback => sub { $ran_circle_radius_observer = 1; }, ); my $circle_obs_id = $circle_obs->id; my $ran_circle_obs_delete_obs = 0; my $subscription = $circle_obs->class->create_subscription( id => $circle_obs->id, method => 'delete', callback => sub { $ran_circle_obs_delete_obs = 1; }, note => "$circle_obs", ); my $observer_observer = UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $subscription->[1]); ok($circle_obs->isa('UR::Observer'), 'added an observer on the circle'); is(UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'), $circle_obs, 'Can get the observer on the circle with get()'); my $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id}; ok($circle_sub, 'adding observer inserted a callback into the Context data structure for callbacks'); is(UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs->id, aspect => 'delete'), $observer_observer, 'Can get the observer on the original observer deletion with get()'); ok($circle_trans->rollback(), 'rolled back transaction'); ok($ran_circle_obs_delete_obs == 0, 'rollback did not run the delete observer'); # because it's creation was undone before the radius observer was deleted $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id}; ok(!$circle_sub, 'rolling back transaction (and with it the observer) removed the subscription'); ok($circle_obs->isa('UR::DeletedRef'), 'radius observer is now a DeletedRef'); ok(! UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'), 'get() no longer returns the circle observer'); ok(! UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs_id, aspect => 'delete'), 'get() no longer returns the observer observer'); $ran_circle_obs_delete_obs = 0; $circle->radius(1); is($ran_circle_obs_delete_obs, 0, 'The circle radius observer did not run'); }; # Verify Transaction Rollback Observer Runs { $circle->radius(3); ok($circle->radius == 3, "original radius is three"); my $transaction = UR::Context::Transaction->begin(); my $observer_ran = 0; add_test_observer('rollback', $transaction, \$observer_ran); my $sub = $UR::Context::all_change_subscriptions->{'UR::Context::Transaction'}->{rollback}->{$transaction->id}; ok($sub, 'adding observer also create change subscription'); ok($transaction->isa('UR::Context::Transaction'), "created first transaction (to test rollback observer)"); ok(!$observer_ran, "observer rollback flag reset to 0"); $circle->radius(5); ok($circle->radius == 5, "in transaction (rollback test), radius is five"); ok($transaction->rollback(), "ran transaction rollback"); ok($observer_ran, "rollback observer ran successfully"); ok($circle->radius == 3, "after rollback, radius is three"); }; # Verify Transaction Commit Observer Runs { $circle->radius(4); ok($circle->radius == 4, "original radius (commit test) is four"); my $transaction = UR::Context::Transaction->begin(); my $observer_ran = 0; add_test_observer('commit', $transaction, \$observer_ran); ok($transaction->isa('UR::Context::Transaction'), "created second transaction (to test commit observer)"); ok(!$observer_ran, "observer rollback flag reset to 0"); $circle->radius(6); ok($circle->radius == 6, "in transaction (commit test), radius is six"); ok($transaction->commit(), "ran transaction commit"); ok($observer_ran, "commit observer ran successfully"); ok($circle->radius == 6, "after commit, radius is six"); # Trying to Rollback a Committed Transaction Fails ok($transaction->state eq 'committed', "transaction is already committed"); my $rv= eval {$transaction->rollback()} || 0; ok($rv == 0, "properly failed transaction rollback for already committed transaction"); }; # Test Nested Transactions { $circle->radius(3); ok($circle->radius == 3, "original radius is 3"); my $outer_transaction = UR::Context::Transaction->begin(); my $outer_observer_ran = 0; add_test_observer('rollback', $outer_transaction, \$outer_observer_ran); ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction"); ok(!$outer_observer_ran, "outer observer flag reset to 0"); $circle->radius(5); ok($circle->radius == 5, "in outer transaction, radius is 5"); my $inner_transaction = UR::Context::Transaction->begin(); my $inner_observer_ran = 0; add_test_observer('rollback', $inner_transaction, \$inner_observer_ran); ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction"); ok(!$inner_observer_ran, "inner observer flag reset to 0"); $circle->radius(7); ok($circle->radius == 7, "in inner transaction, radius is 7"); ok($inner_transaction->rollback(), "ran inner transaction rollback"); ok($inner_observer_ran, "inner transaction observer ran successfully"); ok($circle->radius == 5, "after inner transaction rollback, radius is 5"); ok($outer_transaction->rollback(), "ran transaction rollback"); ok($outer_observer_ran, "outer transaction observer ran successfully"); ok($circle->radius == 3, "after rollback, radius is 3"); }; # testing inner commit { $circle->radius(4); ok($circle->radius == 4, "original radius is 4"); my $outer_transaction = UR::Context::Transaction->begin(); my $outer_observer_ran = 0; add_test_observer('rollback', $outer_transaction, \$outer_observer_ran); ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction"); ok(!$outer_observer_ran, "outer observer flag reset to 0"); $circle->radius(6); ok($circle->radius == 6, "in outer transaction, radius is 6"); my $inner_transaction = UR::Context::Transaction->begin(); my $inner_observer_ran = 0; add_test_observer('commit', $inner_transaction, \$inner_observer_ran); ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction"); ok(!$inner_observer_ran, "inner observer flag reset to 0"); $circle->radius(8); ok($circle->radius == 8, "in inner transaction, radius is 8"); ok($inner_transaction->commit(), "ran inner transaction commit"); ok($inner_observer_ran, "inner transaction observer ran successfully"); ok($circle->radius == 8, "after inner transaction commit, radius is 8"); ok($outer_transaction->rollback(), "ran transaction rollback"); ok($outer_observer_ran, "outer transaction observer ran successfully"); ok($circle->radius == 4, "after rollback, radius is 4"); }; done_testing(); 1; 99_transaction.t100664023532023421 2757312544604517 16107 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; #BEGIN { $ENV{UR_CONTEXT_BASE} = "URT::Context::Testing" }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use DBI; use IO::Pipe; use Test::More; use UR::Value::SloppyPrimitive; use UR::Value::SCALAR; our @test_input = ( ["URT::Foo" => "f1","f2"], ["URT::Bar" => "b1","b2"], ); our $num_classes = scalar(@test_input); our $num_trans = 5; if ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'slow and not needed at install, just at dev time'; } else { plan tests => ((($num_trans * 6) * $num_classes) + 1); } use Data::Dumper; use Data::Compare; # With Purity on (which UR::Util::deep_copy does), Data::Dumper::Dumper complains when it # encounters code refs with no way to disable the warning message. This is an underhanded # way of disabling it. use Carp; $Data::Dumper::Useperl = 1; { no warnings 'redefine'; *Data::Dumper::carp = sub { 1; }; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use UR::Change; use UR::Context; use UR::Context::Transaction; use UR::DataSource; sub dump_states { my ($before,$after); use YAML; #$DB::single = 1; IO::File->new(">before.yml")->print(YAML::Dump($before)); IO::File->new(">after.yml")->print(YAML::Dump($after)); } note("this is a slow test because it copies does deep diffs of large data trees at each step"); ########################################### sub take_state_snapshot { my $state = {}; my $cx = $UR::Context::current; my @classes = sort UR::Object->subclasses_loaded; for my $class_name (@classes) { next if $class_name->isa("UR::Singleton"); my @objects = sort { $a->id cmp $b->id } $cx->all_objects_loaded_unsubclassed($class_name); next unless @objects; next if $class_name eq "UR::Object::Index"; next if $class_name eq "UR::Namespace::CommandParam"; next if $class_name =~ /UR::BoolExpr.*/; next if $class_name eq 'UR::Context::Transaction'; next if $class_name eq 'UR::Change'; next if $class_name->isa("UR::Value"); for my $object (@objects) { next if $class_name->isa("UR::Object::Type") and $object->class_name->isa("UR::Value::Type"); next if $class_name->isa("UR::Value::Type"); $state->{$class_name} ||= {}; my $copy = UR::Util::deep_copy($object); delete $copy->{_change_count}; delete $copy->{_request_count}; delete $copy->{__get_serial}; if ($class_name->isa('UR::Object::Type')) { delete $copy->{get_composite_id_decomposer}; delete $copy->{_ordered_inherited_class_names}; delete $copy->{_all_property_type_names}; delete $copy->{'_unique_property_sets'}; delete $copy->{_all_property_names}; delete $copy->{_all_id_property_names}; delete $copy->{_id_property_sorter}; delete $copy->{_id_property_names}; delete $copy->{_sorter}; delete $copy->{_property_meta_for_name}; delete $copy->{db_committed}{_id_property_sorter}; delete $copy->{db_committed}{_property_meta_for_name}; delete $copy->{db_committed}{_sorter}; delete $copy->{get_composite_id_resolver}; delete $copy->{_property_name_class_map}; delete $copy->{_resolve_property_aliases}; delete $copy->{cache}; } if ($class_name->isa('UR::Object::Property')) { delete $copy->{_is_numeric}; delete $copy->{_data_type_as_class_name}; delete $copy->{_get_property_name_pairs_for_join}; } for my $key (keys %$copy) { if (! defined $copy->{$key}) { delete $copy->{$key}; } elsif (ref($copy->{$key}) eq "ARRAY") { for my $value (@{ $copy->{$key} }) { $value = "CODE REPLACEMENT" if ref($value) eq "CODE"; } } elsif (ref($copy->{$key}) eq "HASH") { for my $key (keys %{ $copy->{$key} }) { $copy->{$key} = "CODE REPLACEMENT" if ref($copy->{$key}) eq "CODE"; } } elsif (ref($copy->{$key}) eq "CODE") { $copy->{$key} = "CODE REPLACEMENT"; } } $state->{$class_name}{$object->id} = $copy; } } return $state; } # These represent the state of the test, and are managed by the subs below. my ($o0, $o1, $o2, $o3, $o4, $o5, $o6, $o7, $o8); my ($state_initial, $state_final); my @transactions; my @transaction_prior_states; my $test_obj_id; sub clear { # wipe everything, reset the id for test objects UR::Context->rollback(); UR::Context->clear_cache(); ($o0, $o1, $o2, $o3, $o4, $o5, $o6, $o7, $o8) = (); ($state_initial, $state_final) = (); @transactions = (); @transaction_prior_states = (); $test_obj_id = 100; } sub init { my ($class_to_test, $property1, $property2) = @_; # pre-transactions: take a snapshot $state_initial = take_state_snapshot(); # make some changes before starting any transactions # these should never be reversed $o0 = $class_to_test->create(id => $test_obj_id, $property1 => 'value0'); ## t0 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o1 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value1"); $o2 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value2"); $o3 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value3"); ## t1 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o2->delete; $o3->$property1("value3changed"); $o4 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value4"); ## t2 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); # change an old unchanged $o4->$property1("value4changed"); # change a different part of a changed object $o3->$property2("value3${property2}changed"); #UR::Context->_sync_databases(); # change a new object $o5 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value5"); $o5->$property1("value5changed"); # change something twice $o6 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value6"); $o6->$property2("value6changed1"); $o6->$property2("value6changed2"); # make something new and then delete it in the same transactions $o7 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value7"); $o7->delete; ## t3 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); # re-create deleted object $o8 = $class_to_test->create(id => $test_obj_id, $property1 => "value8recreated7"); # delete changed object $o6->delete(); ## t4 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o8->delete(); # post-transactions: get a final snapshot $state_final = take_state_snapshot(); } sub rollback_and_verify { my $n = shift; my $msg = shift; my $t = $transactions[$n]; ok($t->rollback, "rolled back transactions $n " . $msg); my $state_now = take_state_snapshot(); my $state_then = $transaction_prior_states[$n]; is_deeply($state_now, $state_then, "application state now matches pre-transaction state for $n " . $msg) or diag(compare_snapshots($state_then,$state_now)); #$DB::single = 1; print ""; } ########################################### # find or create each class we'll test for my $spec (@test_input) { my ($class_name, @property_names) = @$spec; if (UR::Object::Type->get($class_name)) { next; } UR::Object::Type->define( class_name => $class_name, has => \@property_names ); # this dynamically loads, but messes up diffs because of it. #$class_name->generate_support_class("Ghost"); } # ensure that the logic in clear() really takes us back to the starting point my $state_at_test_start = take_state_snapshot(); #$DB::single = 1; clear(); my $state_after_initial_clear = take_state_snapshot(); is_deeply($state_after_initial_clear, $state_at_test_start, "clear returns restores state with no changes"); #dump_states($state_at_test_start,$state_after_initial_clear); # test each specified class for my $test_class_data (@test_input) { my ($test_class_name, @test_property_names) = @$test_class_data; # test clear with this class init($test_class_name, @test_property_names); clear(); my $state_after_first_init_and_clear_for_class = take_state_snapshot(); #$DB::single=1; is_deeply( $state_after_first_init_and_clear_for_class, $state_after_initial_clear, "clear returns restores state after init" ); init($test_class_name, @test_property_names); clear(); my $state_after_second_init_and_clear_for_class = take_state_snapshot(); is_deeply( $state_after_second_init_and_clear_for_class, $state_after_first_init_and_clear_for_class, "clear returns restores state after repeated init" ); # ensure we really are getting a different set of state snapshots # this really only needs to be done once, but requires init() clear(); init($test_class_name, @test_property_names); is(scalar(@transactions), $num_trans, "got the expected number of transactions for the test plan: $num_trans"); is(scalar(@transaction_prior_states), $num_trans, "got the expected number of state snapshots for the test plan: $num_trans"); # sanity check the structures against the plan my $matching_states_found = eval { for my $state_a ($state_initial, @transaction_prior_states,$state_final) { for my $state_b ($state_initial, @transaction_prior_states,$state_final) { next if $state_a == $state_b; if (Compare($state_a,$state_b)) { return 1; } } } return 0; }; ok(!$matching_states_found, "all state snapshots differ from each other"); # ensure we get the _same_ different set each init(). my @expected_states = @transaction_prior_states; clear(); init($test_class_name, @test_property_names); for my $n (0 .. $#transaction_prior_states) { my $expected = $expected_states[$n]; my $actual = $transaction_prior_states[$n]; #my $match = Compare($expected,$actual); #print "match is $match\n"; is_deeply($expected, $actual, "states match for snapshot $n") or diag(compare_snapshots($expected,$actual)); } # test rollback, finally # simple walk backward through transactions for (my $n = $num_trans-1; $n >= 0; $n--) { rollback_and_verify($n, " with later transactions already rolled-back on $test_class_name"); } # ensure rolling back multiple transactions works #for (my $n = 0; $n <= $num_trans; $n++) { for (my $n = $num_trans-1; $n >= 0; $n--) { clear(); init($test_class_name, @test_property_names); rollback_and_verify($n, " with later transactions forcibly rolled-back on $test_class_name"); } } sub compare_snapshots { my ($s1, $s2) = @_; my $f1 = "/tmp/t99-$$.f1"; my $f2 = "/tmp/t99-$$.f2"; IO::File->new(">$f1")->print(YAML::Dump($s1)); IO::File->new(">$f2")->print(YAML::Dump($s2)); #system "opendiff $f1 $f2"; return `sdiff -s $f1 $f2`; } 99_transaction_change_log_order.t100664023532023421 455712544604517 21425 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 2; use Test::Fatal qw(exception); use UR; # This is a regression test to reveal that changes were being recorded in the # wrong order under certain conditions. The observer causes the creation of a # Part via 'first_part_name' to trigger the Machine object to have a property # change on 'part_count'. Since the change of 'part_count' would get logged # before the 'create' of the Machine when the transaction rolled back the undo # for the 'part_count' change would crash due to running after the 'create' was # undone. setup(); Part->add_observer( aspect => 'create', callback => sub { my ($object, $aspect) = @_; my $machine = $object->machine; my $count = $machine->part_count || 0; $machine->part_count($count + 1); }, ); my $tx = UR::Context::Transaction->begin(); my $m = Machine->create(first_part_name => 'King'); my @changes = $tx->get_changes; my $machine_create_change = (grep { $_->changed_class_name eq 'Machine' && $_->changed_aspect eq 'create' } @changes)[0]; my $part_create_change = (grep { $_->changed_class_name eq 'Part' && $_->changed_aspect eq 'create' } @changes)[0]; ok($machine_create_change->id < $part_create_change->id, 'machine should be created before part'); ok(!exception { $tx->rollback }, 'rollback should not throw an exception'); sub setup { my $machine_class = UR::Object::Type->define( class_name => 'Machine', id_generator => '-uuid', id_by => [ serial => { is => 'Text' }, ], has => [ part_count => { is => 'Number' }, first_part_name => { is => 'Part', via => 'parts', to => 'name', where => [ 'serial' => 1 ], is_delegated => 1, is_mutable => 1, }, parts => { is => 'Part', is_many => 1, reverse_as => 'machine', }, ], ); my $part_class = UR::Object::Type->define( class_name => 'Part', id_generator => '-uuid', id_by => [ machine => { is => 'Machine', id_by => 'machine_serial', }, serial => { is => 'Text' }, ], has => [ name => { is => 'Text' }, ], ); } 99_transaction_eval_or_do.t100664023532023421 437012544604517 20246 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 18; use UR; use UR::Context::Transaction; UR::Object::Type->define( class_name => 'Thing', ); is(thing_count(), 0, 'got 0 Things'); Thing->create(); is(thing_count(), 1, 'got 1 Thing'); my $message = 'Something happened!'; # eval dies my $dead_thing = UR::Context::Transaction::eval { Thing->create(); is(thing_count(), 2, 'got 2 Things'); die $message; }; ok(!$dead_thing, 'got no return from eval (die)'); is(thing_count(), 1, 'got 1 Thing after eval (die)'); # eval does not die my $live_thing = UR::Context::Transaction::eval { my $thing = Thing->create(); is(thing_count(), 2, 'got 2 Things'); return $thing; }; isa_ok($live_thing, 'Thing', 'return'); is(thing_count(), 2, 'got 2 Things after eval (success)'); # do dies eval { UR::Context::Transaction::do { Thing->create(); is(thing_count(), 3, 'got 3 Things'); die $message; }; }; my $eval_error = $@; like($eval_error, qr/^$message/, 'got expected eval error'); is(thing_count(), 2, 'got 2 Things after do (die)'); # do returns false eval { UR::Context::Transaction::do { Thing->create(); is(thing_count(), 3, 'got 3 Things'); return; }; }; $eval_error = $@; is($eval_error, '', 'did not get an eval error'); is(thing_count(), 2, 'got 2 Things after do (return)'); # do does not die and does not return false UR::Context::Transaction::do { Thing->create(); is(thing_count(), 3, 'got 3 Things'); }; is(thing_count(), 3, 'got 3 Things'); eval { no warnings 'redefine'; local *UR::Context::Transaction::commit = sub { return }; eval { UR::Context::Transaction::eval { return 1; }; }; my $eval_error = $@; like($eval_error, qr/^failed to commit transaction/, 'got exception if eval fails to commit'); }; eval { no warnings 'redefine'; local *UR::Context::Transaction::rollback = sub { return }; eval { UR::Context::Transaction::eval { die 'trigger rollback'; }; }; my $eval_error = $@; like($eval_error, qr/^failed to rollback transaction/, 'got exception if eval fails to rollback'); }; #### sub thing_count { my @things = Thing->get(); return scalar(@things); } 99_transaction_log_all_changes.t100664023532023421 312212544604517 21230 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 2; use_ok('UR::Context::Transaction'); subtest 'ensure log_all_changes is turned off after last transaction' => sub { plan tests => 10; is(scalar(() = UR::Context::Transaction->get()), 0, 'no transactions at start'); is($UR::Context::Transaction::log_all_changes, 0, 'log_all_changes is disabled at start'); my @tx = UR::Context::Transaction->begin(); is($UR::Context::Transaction::log_all_changes, 1, 'beginning outer transaction enabled log_all_changes'); push @tx, UR::Context::Transaction->begin(); is($UR::Context::Transaction::log_all_changes, 1, 'beginning inner transaction leaves log_all_changes enabled'); pop(@tx)->commit(); is($UR::Context::Transaction::log_all_changes, 1, 'committing inner transaction leaves log_all_changes enabled'); pop(@tx)->commit(); is($UR::Context::Transaction::log_all_changes, 0, 'committing outer transaction disables log_all_changes'); push @tx, UR::Context::Transaction->begin(); is($UR::Context::Transaction::log_all_changes, 1, 'beginning a new first transaction enabled log_all_changes'); push @tx, UR::Context::Transaction->begin(); is($UR::Context::Transaction::log_all_changes, 1, 'beginning inner transaction leaves log_all_changes enabled'); pop(@tx)->rollback(); is($UR::Context::Transaction::log_all_changes, 1, 'rolling back inner transaction leaves log_all_changes enabled'); pop(@tx)->rollback(); is($UR::Context::Transaction::log_all_changes, 0, 'rolling back outer transaction disables log_all_changes'); }; 99_transaction_rollback_after_create.t100664023532023421 76612544604517 22417 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use UR; use Test::More tests => 3; use UR::Test qw(txtest); UR::Object::Type->define( class_name => 'Car', has => [ name => { is => 'Text', }, ], ); is(scalar(() = Car->get()), 0, 'no cars before txtest'); txtest 'confirm rollback works' => sub { plan tests => 1; Car->create(name => 'Christine'); is(scalar(() = Car->get()), 1, 'got one car inside txtest'); }; is(scalar(() = Car->get()), 0, 'no cars after txtest'); 99_transaction_unload.t100664023532023421 215512544604517 17416 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use Test::More tests => 3; subtest 'setup' => sub { plan tests => 3; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh->do('CREATE TABLE person (id integer PRIMARY KEY, name text)'), 'created table (person)'); ok($dbh->do('INSERT INTO person (id, name) VALUES (1, NULL)'), 'inserted person 1'); my $meta = UR::Object::Type->__define__( class_name => 'URT::Person', id_by => [ id => { is => 'Integer' }, ], has => { name => { is => 'Text' }, }, data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); ok($meta, 'defined a class'); }; my $person = URT::Person->get(1); ok(scalar($person->__errors__), 'created a person with errors'); my $tx = UR::Context::Transaction->begin(); URT::Person->unload(); ok($tx->commit, 'committed after unloading erroneous Person'); path_spec_expansion.t100664023532023421 4100212544604517 22403 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 60; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my %people = ( Pyle => { rank => 'Private', serial => 123 }, Bailey => { rank => 'Private', serial => 234 }, Snorkel => { rank => 'Sergent', serial => 345 }, Carter => { rank => 'Sergent', serial => 456 }, Halftrack => { rank => 'General', serial => 567 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, 'Created temp dir'); my $tmpdir_strlen = length($tmpdir); my $dir = $tmpdir . '/extra_dir'; ok(mkdir($dir), 'Created extra_dir within temp dir'); my $dir_strlen = length($dir); while (my($name,$data) = each %people) { ok(_create_data_file($dir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } my $ds = UR::DataSource::Filesystem->create( path => $dir.'/$rank/${name}.dat', columns => ['serial'], ); ok($ds, 'Created data source'); class URT::Thing { has => [ other => { is => 'String' }, other2 => { is => 'String' }, name => { is => 'String' }, rank => { is => 'String' }, serial => { is => 'Number' }, ], data_source_id => $ds->id, }; # First, test the low-level replacement methods for variables # A simple one with single values for both properties my $bx = URT::Thing->define_boolexpr(name => 'Pyle', rank => 'Private'); ok($bx, 'Create boolexpr matching a name and rank'); my @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/$name' ); is(scalar(@data), 1, 'property replacement yielded one pathname'); is_deeply(\@data, [ [ "${dir}/Private/Pyle", { name => 'Pyle', rank => 'Private'} ]], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement yielded one pathname, with extension'); is_deeply(\@data, [ [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private'} ]], 'Path resolution data is correct'); # Give 2 values for each property $bx = URT::Thing->define_boolexpr(rank => ['General','Sergent'], name => ['Pyle','Washington']); ok($bx, 'Create boolexpr matching name and rank with in-clauses'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/$name.dat' ); is(scalar(@data), 4, 'Property replacement yields 4 pathnames'); @data = sort {$a->[0] cmp $b->[0]} @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/General/Washington.dat", { name => 'Washington', rank => 'General' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], [ "${dir}/Sergent/Washington.dat", { name => 'Washington', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This one only supplies a value for one property. It'll have to glob the filesystem for the other value $bx = URT::Thing->define_boolexpr(name => 'Pyle'); ok($bx, 'Create boolexpr with just name'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement yielded one pathname, with extension'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "${dir}/*/Pyle.dat", { name => 'Pyle', '.__glob_positions__' => [ [$dir_strlen+1, 'rank' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 3, 'Glob replacement yielded three possible pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This path spec has a hardcoded glob in it already $bx = $bx = URT::Thing->define_boolexpr(name => 'Pyle'); ok($bx, 'Create boolexpr with just name'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/*/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/Pyle.dat", { name => 'Pyle', '.__glob_positions__' => [ [$tmpdir_strlen+3, 'rank' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 3, 'Glob replacement yielded three possible pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # Make a bx with no filters and two properties in the path spec $bx = $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/*/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+3, 'rank' ],[$tmpdir_strlen+5,'name' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # a bx with no filters and three properties in the path spec $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/$other/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+1, 'other' ], [$tmpdir_strlen+3,'rank'], [$tmpdir_strlen+5,'name' ], ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra_dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra_dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra_dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra_dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra_dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This one has multiple variables in the same path portion $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/${other}_${other2}/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*_*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+1, 'other' ], [$tmpdir_strlen+3, 'other2' ], [$tmpdir_strlen+5,'rank'], [$tmpdir_strlen+7,'name' ], ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', other2 => 'dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', other2 => 'dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', other2 => 'dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', other2 => 'dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', other2 => 'dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # Try it on a method call my $is_sub_called = 0; my $bx_from_sub; my $class_from_sub; my $resolver = sub { my($class,$rule) = @_; $class_from_sub = $class; $bx_from_sub = $bx; $is_sub_called++; return 'extra_dir'; }; Sub::Install::install_sub({ code => $resolver, into => 'URT::Thing', as => 'extra_path_resolver' }); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/General/Halftrack.dat' ); is(scalar(@data), 1, 'property replacement for spec including a method call yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); is($is_sub_called, 1, 'The resolver sub was called'); is($class_from_sub, 'URT::Thing', 'The resolver sub was passed the right class name'); is($bx_from_sub, $bx, 'The resolver sub was passed the right boolexpr'); # pair of method calls Sub::Install::install_sub({ code => sub { 'dat' }, into => 'URT::Thing', as => 'data_file_extension'}); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/General/Halftrack.&data_file_extension' ); is(scalar(@data), 1, 'property replacement for spec including two method calls yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # pair of methods in the same path part Sub::Install::install_sub({ code => sub { 'extra' }, into => 'URT::Thing', as => 'extra_word'}); Sub::Install::install_sub({ code => sub { 'dir' }, into => 'URT::Thing', as => 'dir_word'}); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&{extra_word}_&{dir_word}/General/Halftrack.&data_file_extension' ); is(scalar(@data), 1, 'property replacement for spec including three yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # method call returning multiple values Sub::Install::install_sub({ code => sub { return ('General','Private','Sergent') }, into => 'URT::Thing', as => 'rank_list'}); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/&rank_list/*.&data_file_extension' ); is(scalar(@data), 3, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/*.dat", {'.__glob_positions__' => []} ], [ "$tmpdir/extra_dir/Private/*.dat", {'.__glob_positions__' => []} ], [ "$tmpdir/extra_dir/Sergent/*.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # put it all together # a bunch of variables $bx = URT::Thing->define_boolexpr(); @data = UR::DataSource::Filesystem->resolve_file_info_for_rule_and_path_spec($bx, ${tmpdir}.'/${other}_${other2}/$rank/${name}.dat'); is(scalar(@data), 5, 'resolve_file_info_for_rule_and_path_spec() returns 5 pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', other2 => 'dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', other2 => 'dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', other2 => 'dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', other2 => 'dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', other2 => 'dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # variables, methods and globs $bx = URT::Thing->define_boolexpr(); @data = UR::DataSource::Filesystem->resolve_file_info_for_rule_and_path_spec( $bx, ${tmpdir}.'/${other}_&dir_word/$rank/${name}.&data_file_extension' ); is(scalar(@data), 5, 'resolve_file_info_for_rule_and_path_spec() returns 5 pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); 1; sub _create_data_file { my($dir,$rank,$name,$data) = @_; my $subdir = $dir . '/' . $rank; unless (-d $subdir) { mkdir $subdir || die "Can't create subdir $subdir: $!"; } my $pathname = $subdir . '/' . $name . '.dat'; my $f = IO::File->new($pathname, 'w') || die "Can't create file $pathname: $!"; $f->print($data); 1; } read.t100664023532023421 710112544604517 17246 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 21; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my @people = ( Hudson => { rank => 'Sergent', serial => 499 }, Bob => { rank => 'General', serial => 678 }, Carter => { rank => 'Sergent', serial => 456 }, Snorkel => { rank => 'Sergent', serial => 345 }, Bailey => { rank => 'Private', serial => 234 }, Halftrack => { rank => 'General', serial => 567 }, Pyle => { rank => 'Private', serial => 123 }, Hudson => { rank => 'Private', serial => 299 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); for (my $i = 0; $i < @people; $i += 2) { my $name = $people[$i]; my $data = $people[$i+1]; ok(_create_data_file($tmpdir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } ok(UR::Object::Type->define( class_name => 'URT::Soldier', id_by => [ serial => { is => 'Number' } ], has => [ name => { is => 'String' }, rank => { is => 'String' }, ], #data_source => { uri => "file:$tmpdir/\$rank.dat" } data_source => { is => 'UR::DataSource::Filesystem', path => $tmpdir.'/$rank.dat', columns => ['name','serial'], delimiter => "\t", }, ), 'Defined class for soldiers'); my @objs = URT::Soldier->get(name => 'Pyle', rank => 'Private'); is(scalar(@objs), 1, 'Got one Private named Pyle'); ok(_compare_to_expected($objs[0], { name => 'Pyle', rank => 'Private', serial => 123} ), 'Object has the correct data'); @objs = URT::Soldier->get(rank => 'General'); is(scalar(@objs), 2, 'Got two soldiers with rank General'); ok(_compare_to_expected($objs[0], { name => 'Halftrack', rank => 'General', serial => 567 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Bob', rank => 'General', serial => 678 }), 'Second object has correct data'); @objs = URT::Soldier->get(name => 'no one'); is(scalar(@objs), 0, 'Found no soldiers named "no one"'); @objs = URT::Soldier->get(name => 'Hudson'); is(scalar(@objs), 2, 'Matched two soldiers named Hudson'); ok(_compare_to_expected($objs[0], { name => 'Hudson', rank => 'Private', serial => 299 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Hudson', rank => 'Sergent', serial => 499 }), 'Second object has correct data'); @objs = URT::Soldier->get(456); is(scalar(@objs), 1, 'Got 1 soldier by ID'); ok(_compare_to_expected($objs[0], { name => 'Carter', rank => 'Sergent', serial => 456 }), 'Object has correct data'); sub _compare_to_expected { my($obj,$expected) = @_; return unless $obj->name eq $expected->{'name'}; return unless $obj->id eq $expected->{'serial'}; return unless $obj->serial eq $expected->{'serial'}; return unless $obj->rank eq $expected->{'rank'}; return 1; } sub _create_data_file { my($dir,$rank,$name,$serial) = @_; my $pathname = $dir . '/' . $rank . '.dat'; my $f = IO::File->new($pathname, '>>'); die "Can't create file $pathname: $!" unless $f; $f->print("$name\t$serial\n"); $f->close; 1; } read_columns_from_header.t100664023532023421 1020512544604517 23360 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 21; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my @people = ( Pyle => { rank => 'Private', serial => 123 }, Bailey => { rank => 'Private', serial => 234 }, Hudson => { rank => 'Private', serial => 299 }, Snorkel => { rank => 'Sergent', serial => 345 }, Carter => { rank => 'Sergent', serial => 456 }, Hudson => { rank => 'Sergent', serial => 499 }, Halftrack => { rank => 'General', serial => 567 }, Bob => { rank => 'General', serial => 678 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); for (my $i = 0; $i < @people; $i += 2) { my $name = $people[$i]; my $data = $people[$i+1]; ok(_create_data_file($tmpdir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } ok(UR::Object::Type->define( class_name => 'URT::Soldier', id_by => [ serial => { is => 'Number' } ], has => [ name => { is => 'String' }, rank => { is => 'String' }, ], #data_source => { uri => "file:$tmpdir/\$rank.dat" } data_source => { is => 'UR::DataSource::Filesystem', path => $tmpdir.'/$rank.dat', columns_from_header => 1, header_lines => 2, delimiter => "\t", }, ), 'Defined class for soldiers'); my @objs = URT::Soldier->get(name => 'Pyle', rank => 'Private'); is(scalar(@objs), 1, 'Got one Private named Pyle'); ok(_compare_to_expected($objs[0], { name => 'Pyle', rank => 'Private', serial => 123} ), 'Object has the correct data'); @objs = URT::Soldier->get(rank => 'General'); is(scalar(@objs), 2, 'Got two soldiers with rank General'); ok(_compare_to_expected($objs[0], { name => 'Halftrack', rank => 'General', serial => 567 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Bob', rank => 'General', serial => 678 }), 'Second object has correct data'); @objs = URT::Soldier->get(name => 'no one'); is(scalar(@objs), 0, 'Found no soldiers named "no one"'); @objs = URT::Soldier->get(name => 'Hudson'); is(scalar(@objs), 2, 'Matched two soldiers named Hudson'); ok(_compare_to_expected($objs[0], { name => 'Hudson', rank => 'Private', serial => 299 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Hudson', rank => 'Sergent', serial => 499 }), 'Second object has correct data'); @objs = URT::Soldier->get(456); is(scalar(@objs), 1, 'Got 1 soldier by ID'); ok(_compare_to_expected($objs[0], { name => 'Carter', rank => 'Sergent', serial => 456 }), 'Object has correct data'); sub _compare_to_expected { my($obj,$expected) = @_; return unless $obj->name eq $expected->{'name'}; return unless $obj->id eq $expected->{'serial'}; return unless $obj->serial eq $expected->{'serial'}; return unless $obj->rank eq $expected->{'rank'}; return 1; } my %files; my $files_written; BEGIN { $files_written = 0 } sub _create_data_file { my($dir,$rank,$name,$serial) = @_; my $pathname = $dir . '/' . $rank . '.dat'; my $f = IO::File->new($pathname, '>>'); die "Can't create file $pathname: $!" unless $f; my $write_order; unless (defined( $write_order = $files{$pathname})) { # First time writing to this file. Put in the header # mix it up - half of the files will have name first, half will have serial first $write_order = $files{$pathname} = $files_written % 2; $write_order ? $f->print("name\tserial\n----\t------\n") : $f->print("serial\tname\n------\t----\n"); $files_written++; } $write_order ? $f->print("$name\t$serial\n") : $f->print("$serial\t$name\n"); $f->close; 1; } read_efficiency.t100664023532023421 1620412544604517 21456 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 32; use IO::File; use File::Temp; # The file tracking stuff is defined at the bottom of this file my ($file_new, $file_open, $file_close, $file_DESTROY, $file_seek, $file_seek_pos, $file_tell, $file_getline); IO::File::Tracker->config_callbacks( 'new' => sub { no warnings 'uninitialized'; $file_new++ }, 'open' => sub { no warnings 'uninitialized'; $file_open++ }, 'close' => sub { no warnings 'uninitialized'; $file_close++ }, 'DESTROY' => sub { no warnings 'uninitialized'; $file_DESTROY++ }, 'seek' => sub { no warnings 'uninitialized'; $file_seek_pos = $_[0]; $file_seek++ }, 'tell' => sub { no warnings 'uninitialized'; $file_tell++ }, 'getline' => sub { no warnings 'uninitialized'; $file_getline++ }, ); sub clear_trackers { $file_new = 0; $file_open = 0; $file_close = 0; $file_DESTROY = 0; $file_seek = 0; $file_seek_pos = undef; $file_tell = 0; $file_getline = 0; }; # File data: id name is_upper my @data = ( [1, 'AAA', 1], [2, 'BBB', 1], [3, 'CCC', 1], [4, 'DDD', 1], [5, 'EEE', 1], [6, 'fff', 0], [7, 'ggg', 0], [8, 'hhh', 0], [9, 'iii', 0], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", handle_class => 'IO::File::Tracker', columns => ['letter_id','name','is_upper'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Letter', id_by => [ letter_id => { is => 'Number' } ], has => [ name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $data_source->id, ), 'Defined class for letters'); my @file_columns_in_order = ('id','name','is_upper'); my %sorters; foreach my $cols ( [letter_id => 0], [name => 1], [is_upper => 2] ) { my($key,$col) = @$cols; $sorters{$key} = sub { no warnings 'numeric'; $a->[$col] <=> $b->[$col] or $a->[$col] cmp $b->[$col] }; } foreach my $cols ( [letter_id => 0], [name => 1], [is_upper => 2] ) { my($key,$col) = @$cols; $sorters{'-'.$key} = sub { no warnings 'numeric'; $b->[$col] <=> $a->[$col] or $b->[$col] cmp $a->[$col] }; } my($write_sorter, @write_data, @matches, @results, @expected, $sorter_sub); # First, write out the file in id-sorted order. # Don't tell the data source about any particular sorting. &clear_trackers(); $sorter_sub = $sorters{'letter_id'}; @write_data = sort $sorter_sub @data; ok(save_data_to_file($datafile, \@write_data), 'Save file in id-sorted order'); @matches = URT::Letter->get(1); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 1, 'Got one result matching id 1'); is_deeply($results[0], [ 1, 'AAA', 1], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 10, 'getline() was called 10 times'); # One additional at the end of the file is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['letter_id']), 'Configure the data source to be sorted by letter_id'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get(1); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 1, 'Got one result matching id 1'); is_deeply($results[0], [ 1, 'AAA', 1], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 2, 'getline() was called 2 times'); # had to read the 2nd line to know there were no more matches is($file_DESTROY, 1, 'DESTROY was called one time'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('id <' => 5); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 4, 'Got 4 results with id < 5'); is_deeply(\@results, [ [ 1, 'AAA', 1], [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 5, 'getline() was called 5 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['-is_upper']), 'Configure the data source to be sorted by -is_upper'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('is_upper >' => 0); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 5, 'Got 5 results matching is_upper > 0'); is_deeply(\@results, [ [ 1, 'AAA', 1], [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1], [ 5, 'EEE', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 6, 'getline() was called 6 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['name','-is_upper']), 'Configure the data source to be sorted by name and -is_upper'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('name between' => ['BBB','DDD']); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 3, 'Got 3 results matching name between BBB and DDD'); is_deeply(\@results, [ [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 5, 'getline() was called 5 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); sub save_data_to_file { my($fh, $datalist) = @_; $fh->seek(0,0); $fh->print(map { $_ . "\n" } map { join("\t", @$_) } @$datalist); truncate($fh, $fh->tell()); $fh->flush(); return 1; } package IO::File::Tracker; our %callbacks; sub config_callbacks { my $class = shift; my %set_callbacks = @_; foreach my $key ( keys %set_callbacks) { $callbacks{$key} = $set_callbacks{$key}; } } sub _call_cb { my($op, @args) = @_; my $cb = $callbacks{$op}; if ($cb) { $cb->(@args); } } use vars '$AUTOLOAD'; sub AUTOLOAD { my $subname = $AUTOLOAD; $subname =~ s/^.*:://; my $super = IO::File->can($subname) || IO::Handle->can($subname); if ($super) { $super->(@_); } else { Carp::croak("Can't wrap method $subname because it is not implemented by IO::File"); } } BEGIN { # Create overridden methods for the ones we want to track no strict 'refs'; foreach my $subname (qw( new open close DESTROY seek tell getline ) ) { my $subref = sub { my $self = shift; _call_cb($subname, @_); my $super = IO::File->can($subname); return $super->($self, @_); }; my $fq_subname = 'IO::File::Tracker::'.$subname; *$fq_subname = $subref; } } read_files_as_tables.t100664023532023421 641012544604517 22447 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 25; use IO::File; use File::Temp; use Sub::Install; my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); ok(mkdir($tmpdir."/123/"), 'Create subdir within tmpdir'); my %data = ( 'dogs' => [ [ 1, 'lassie', 11], [ 2, 'benjy', 12 ], [ 3, 'beethoven', 13 ], [ 4, 'ralf', 14 ], ], 'cats' => [ [ 11, 'garfield', 1 ], [ 12, 'nermal', 2 ], [ 13, 'sassy', 3 ], [ 14, 'fluffy', 4 ], ], ); foreach my $species ( keys %data ) { my $pathname = "${tmpdir}/123/${species}.dat"; my $fh = IO::File->new($pathname, 'w') || die "Can't open $pathname for writing: $!"; foreach my $animal ( @{ $data{$species} } ) { $fh->print(join("\t", @$animal) . "\n"); } ok($fh->close(), "wrote info for $pathname"); } my $ds = UR::DataSource::Filesystem->create(path => $tmpdir.'/$group/', columns => ['id','name', 'friend_id'], delimiter => "\t"); ok($ds, 'Created Filesystem datasource'); ok(UR::Object::Type->define( class_name => 'URT::Cat', id_by => [ cat_id => { is => 'Number', column_name => 'id' } ], has => [ group => { is => 'Number' }, name => { is => 'String' }, friend_id => { is => 'Number' }, ], data_source_id => $ds->id, table_name => 'cats.dat' ), 'Defined class for cats'); ok(UR::Object::Type->define( class_name => 'URT::Dog', id_by => [ dog_id => { is => 'Number', column_name => 'id' } ], has => [ group => { is => 'Number' }, name => { is => 'String' }, friend => { is => 'URT::Cat', id_by => 'friend_id' }, friend_name => { via =>'friend', to => 'name' }, ], data_source_id => $ds->id, table_name => 'dogs.dat' ), 'Defined class for dogs'); my @objs = URT::Dog->get(name => 'benjy'); is(scalar(@objs), 1,'Got one dog named benjy'); is($objs[0]->id, 2, 'It has the right id'); is($objs[0]->name, 'benjy', 'It has the right id'); is($objs[0]->friend_id, 12, 'It has the right friend id'); @objs = $objs[0]->friend; is(scalar(@objs), 1, 'it has one friend'); is($objs[0]->id, 12, 'with the right ID'); is($objs[0]->name, 'nermal', 'and the right name'); @objs = URT::Dog->get('id <' => 3); is(scalar(@objs), 2, 'Got 3 dogs with ID < 3'); is($objs[0]->id, 1, 'First has the right ID'); is($objs[1]->id, 2, 'Second has the right ID'); my $cat = URT::Cat->get(name => 'sassy'); ok($cat, 'Got one cat named sassy'); is($cat->name, 'sassy', 'It was the right cat'); @objs = URT::Dog->get(friend => $cat); is(scalar(@objs), 1, 'There is one dog whose friend is sassy'); is($objs[0]->id, 3, 'its ID is correct'); is($objs[0]->name, 'beethoven', 'its name is correct'); @objs = URT::Dog->get(friend_name => 'fluffy'); is(scalar(@objs), 1, 'Got one dog whose friend name is fluffy'); is($objs[0]->id, 4, 'Its ID is correct'); is($objs[0]->name, 'ralf', 'Its name is correct'); read_linenum_as_column.t100664023532023421 447612544604517 23051 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 29; use IO::File; use File::Temp; # First write some easy data my $fh = File::Temp->new(); foreach ( 'a','b','c','d','e' ) { $fh->print($_,"\n"); } $fh->close(); my $filename = $fh->filename; ok(UR::Object::Type->define( class_name => 'URT::Alphabet', id_by => [ file => { is => 'String', column_name => '__FILE__'}, lineno => { is => 'Integer', column_name => '$.' }, ], has => [ letter => { is => 'String' }, ], data_source => { is => 'UR::DataSource::Filesystem', path => '$file', columns => ['letter'], }, ), 'Defined class for letters'); my @objs = URT::Alphabet->get(file => $filename, 'lineno <' => 4); is(scalar(@objs), 3, 'Got 3 objects back filtering by lineno < 4'); # because line numbers ($.) start at 1 my @expected = ( { file => $filename, lineno => 1, letter => 'a' }, { file => $filename, lineno => 2, letter => 'b' }, { file => $filename, lineno => 3, letter => 'c' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } @objs = URT::Alphabet->get(file => $filename, lineno => 4); is(scalar(@objs), 1, 'Got 1 object with lineno == 4'); _compare_to_expected($objs[0], { file => $filename, lineno => 4, letter => 'd' }); @objs = URT::Alphabet->get(file => $filename, lineno => 10); is(scalar(@objs), 0, 'Correctly got 0 objects with lineno == 10'); @objs = URT::Alphabet->get(file => $filename, 'lineno between' => [2,7]); is(scalar(@objs), 4, 'Got 4 objects with lineno between 2 and 7'); @expected = ( { file => $filename, lineno => 2, letter => 'b' }, { file => $filename, lineno => 3, letter => 'c' }, { file => $filename, lineno => 4, letter => 'd' }, { file => $filename, lineno => 5, letter => 'e' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } sub _compare_to_expected { my($obj,$expected) = @_; foreach my $prop ( 'file','lineno','letter' ) { is($obj->$prop, $expected->{$prop}, "$prop has expected value"); } } read_multichar_record_sep.t100664023532023421 357112544604517 23532 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 8; use IO::File; use File::Temp; use Sub::Install; # Test the case where the record separator is a multi-character string, and # make sure that the last record in the file does not match that whole string # Make a FASTQ-like format file intentionally missing a blank line at the end my $fh = File::Temp->new(); my $data = "read1 ACGTTGCA + 12345678 abc read2 GAAGTCCT + 87654321 a"; $fh->print($data); $fh->close; ok(UR::Object::Type->define( class_name => 'URT::FastqReads', id_by => [ path => { is => 'String', column_name => '__FILE__' }, record => { is => 'Integer', column_name => '$.' }, ], has => [ seq_id => { is => 'String'}, sequence => { is => 'String' }, quality => { is => 'String' }, ], data_source => { is => 'UR::DataSource::Filesystem', path => '$path', columns => ['seq_id','sequence', undef, 'quality'], delimiter => "\n", record_separator => "\nabc\n", }, ), 'Defined class for fastq reads'); my @objs = URT::FastqReads->get(path => $fh->filename); is(scalar(@objs), 2, 'Read in 1 records from the fastq file'); my @expected = ( { seq_id => 'read1', sequence => 'ACGTTGCA', quality => '12345678' }, { seq_id => 'read2', sequence => 'GAAGTCCT', quality => '87654321' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } sub _compare_to_expected { my($obj,$expected) = @_; foreach my $prop ( keys %$expected ) { is($obj->$prop, $expected->{$prop}, "property $prop is correct"); } return 1; } read_order_by.t100664023532023421 751512544604517 21144 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 83; use IO::File; use File::Temp; # File data: id name score color my @data = ( [1, 'one', 10,'red'], [2, 'two', 10,'green'], [3, 'three', 9, 'blue'], [4, 'four', 9, 'black'], [5, 'five', 8, 'yellow'], [6, 'six', 8, 'white'], [7, 'seven', 7, 'purple'], [8, 'eight', 7, 'orange'], [9, 'nine', 6, 'pink'], [10, 'ten', 6, 'brown'], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", # handle_class => 'URT::FileTracker', columns => ['thing_id','name','score','color'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Number' } ], has => [ name => { is => 'String' }, score => { is => 'Integer' }, color => { is => 'String'}, ], data_source_id => $data_source->id, ), 'Defined class for things'); my @file_columns_in_order = ('id','name','score','color'); my %sorters; foreach my $cols ( [id => 0], [name => 1], [score => 2], [color => 3] ) { my($key,$col) = @$cols; $sorters{$key} = sub { no warnings 'numeric'; $a->[$col] <=> $b->[$col] or $a->[$col] cmp $b->[$col] }; } foreach my $cols ( [id => 0], [name => 1], [score => 2], [color => 3] ) { my($key,$col) = @$cols; $sorters{'-'.$key} = sub { no warnings 'numeric'; $b->[$col] <=> $a->[$col] or $b->[$col] cmp $a->[$col] }; } foreach my $write_sort_order ( 'asc','desc' ) { foreach my $sortby_col ( 0 .. 3 ) { # The number of columns in @data # sort the data by one of the columns... my %file_write_sorters = ( asc => sub { no warnings 'numeric'; $a->[$sortby_col] <=> $b->[$sortby_col] or $a->[$sortby_col] cmp $b->[$sortby_col] }, desc => sub { no warnings 'numeric'; $b->[$sortby_col] <=> $a->[$sortby_col] or $b->[$sortby_col] cmp $a->[$sortby_col] }, ); my $write_sorter = $file_write_sorters{$write_sort_order}; my @write_data = sort $write_sorter @data; ok(save_data_to_file($datafile, \@write_data), "Saved data sorted by column $sortby_col $write_sort_order $file_columns_in_order[$sortby_col]"); $data_source->sorted_columns( [ ($write_sort_order eq 'desc' ? '-' : '') . $data_source->columns->[$sortby_col] ] ); URT::Thing->unload(); my @results = map { [ @$_{@file_columns_in_order} ] } URT::Thing->get(); my $sort_sub = $sorters{'id'}; my @expected = sort $sort_sub @data; is_deeply(\@results, \@expected, 'Got all objects in default (id) sort order'); foreach my $order_by_direction ( '', '-') { for my $sort_prop ( 'id', 'name', 'score', 'color' ) { URT::Thing->unload(); my $order_by_prop = $order_by_direction . $sort_prop; my @results = map { [ @$_{@file_columns_in_order} ] } URT::Thing->get(-order => [ $order_by_prop]); my $sort_sub = $sorters{$order_by_prop}; my @expected = sort $sort_sub @data; is_deeply(\@results, \@expected, "Got all objects sorted by $order_by_prop in the right order"); } } } } sub save_data_to_file { my($fh, $datalist) = @_; $fh->seek(0,0); $fh->print(map { $_ . "\n" } map { join("\t", @$_) } @$datalist); truncate($fh, $fh->tell()); $fh->flush(); return 1; } write.t100664023532023421 457512544604517 17501 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 15; use IO::File; use File::Temp; # File data: id name score my @data = ( [1, 'AAA', 1], [2, 'BBB', 1], [4, 'DDD', 1], [5, 'EEE', 1], [6, 'fff', 0], [7, 'ggg', 0], [9, 'iii', 0], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); $datafile->print(join("\t",@$_),"\n") foreach (@data); $datafile->flush(); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", columns => ['letter_id','name','score'], sorted_columns => ['name','letter_id'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Letter', id_by => [ letter_id => { is => 'Number' } ], has => [ name => { is => 'String' }, score => { is => 'Number' }, ], data_source_id => $data_source->id, ), 'Defined class for letters'); my $letter_a = URT::Letter->get(name => 'AAA'); ok($letter_a, 'Got Letter named AAA'); ok($letter_a->score(2), 'Changed score to 2'); my $letter_i = URT::Letter->get(name => 'iii'); ok($letter_i, 'Got letter named iii'); ok($letter_i->name('III'), 'Changed name to III'); my $letter_f = URT::Letter->get(name =>'fff'); ok($letter_f, 'Got letter named fff'); ok($letter_f->delete(), 'Delete letter fff'); my $letter_a2 = URT::Letter->create(id => 10, name => 'aaa', score => 2); ok($letter_a2, 'Created new letter named aaa'); my $letter_a3 = URT::Letter->create(id => 11, name => 'AAA', score => 4); ok($letter_a3, 'Created new letter named aaa'); my $letter_z = URT::Letter->create(id => 12, name => 'zzz', score => 6); ok($letter_z, 'Created new letter named zzz'); ok(UR::Context->commit(), 'Commit changes'); my $fh = IO::File->new($datafile->filename); ok($fh, 'Open data file for reading'); my @lines = <$fh>; is_deeply(\@lines, [ "1\tAAA\t2\n", "11\tAAA\t4\n", "2\tBBB\t1\n", "4\tDDD\t1\n", "5\tEEE\t1\n", "9\tIII\t0\n", "10\taaa\t2\n", "7\tggg\t0\n", "12\tzzz\t6\n", ], 'File contents are correct'); mro.t100664023532023421 2510312544604517 14021 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use UR; use MRO::Compat; setup_test_env(); test_namespace_valid_values_mro(); test_namespace_gets_default_mro(); test_gryphon_object_methods_follow_dfs_mro(); test_gryphon_inheritance_follows_dfs_mro(); test_class_property_follows_dfs_mro(); test_package_sub_follows_dfs_mro(); if ($^V lt 5.9.5) { my $namespace = C3Animal->get(); is($namespace->method_resolution_order, 'dfs', 'MRO reverted to DFS on a C3 namespace if Perl < 5.9.5'); note('Skipping C3 tests because Perl < 5.9.5.'); } else { test_gryphon_object_methods_follow_c3_mro(); test_gryphon_inheritance_follows_c3_mro(); test_class_property_follows_c3_mro(); test_package_sub_follows_c3_mro(); } done_testing(); sub test_namespace_valid_values_mro { my $namespace = Animal->get(); my $property = $namespace->__meta__->property('method_resolution_order'); if ($^V lt 5.9.5) { is_deeply($property->valid_values, ['dfs'], 'valid MRO for Perl < 5.9.5 is only DFS'); } else { is_deeply($property->valid_values, ['dfs', 'c3'], 'valid MRO for Perl >= 5.9.5 is DFS and C3'); } } sub test_namespace_gets_default_mro { my $animal_namespace = Animal->get(); isa_ok($animal_namespace, 'UR::Namespace', 'got Animal namespace'); # This is meant to check that the namespace has the default value of method_resolution_order # populated on get unlike non-singleton objects which are only populated on create. ok($animal_namespace->can('method_resolution_order'), 'namespace can method_resolution_order') || return; ok($animal_namespace->method_resolution_order, 'namespace has a method_resolution_order'); } ####################### # DFS Based Namespace # ####################### sub test_gryphon_object_methods_follow_dfs_mro { my $animal = DfsAnimal::Animal->create(); my $lion = DfsAnimal::Lion->create(); my $eagle = DfsAnimal::Eagle->create(); my $gryphon = DfsAnimal::Gryphon->create(); is($lion->foo, $animal->foo, "Lion's foo is the same as Animal's"); isnt($eagle->foo, $animal->foo, "Eagle's foo is not the same as Animal's"); is($gryphon->foo, $animal->foo, "Gryphon's foo is the same as Animal's"); } sub test_gryphon_inheritance_follows_dfs_mro { my $gryphon = DfsAnimal::Gryphon->create(); isa_ok($gryphon, 'DfsAnimal::Gryphon', '$gryphon isa DfsAnimal::Gryphon'); isa_ok($gryphon, 'DfsAnimal::Lion', '$gryphon isa DfsAnimal::Lion'); isa_ok($gryphon, 'DfsAnimal::Eagle', '$gryphon isa DfsAnimal::Eagle'); is(mro::get_mro('DfsAnimal::Gryphon'), 'dfs', "Gryphon's MRO is DFS"); my $i = 0; my $mro_linear_isa = mro::get_linear_isa('DfsAnimal::Gryphon'); my %inheritance = map { $_ => $i++ } @$mro_linear_isa; ok($inheritance{'DfsAnimal::Lion'} < $inheritance{'DfsAnimal::Eagle'}, 'Lion is higher precendence than Eagle'); ok($inheritance{'DfsAnimal::Eagle'} > $inheritance{'UR::Object'}, 'Eagle is lower precendence than UR::Object'); } sub test_class_property_follows_dfs_mro { # This is theoretically the same check as comparing $gryphon->foo to $eagle->foo # However, it appears that property resolution is different than method resolution # since property resolution is done by hand and is probably a breadth first search. my $meta = UR::Object::Type->get(class_name => 'DfsAnimal::Gryphon'); my $foo_property_meta = $meta->property_meta_for_name('foo'); is($foo_property_meta->class_name, 'DfsAnimal::Eagle', "Gryphon is using Eagle's foo"); my $foo_property = $meta->property('foo'); is($foo_property->class_name, 'DfsAnimal::Eagle', "Gryphon is using Eagle's foo"); } sub test_package_sub_follows_dfs_mro { is(DfsAnimal::Animal->species(), 'Animal', "Make sure we installed species sub in Animal"); is(DfsAnimal::Eagle->species(), 'Eagle', "Make sure we installed species sub in Eagle"); is(DfsAnimal::Gryphon->species(), 'Animal', "Gryphon called Animal's species sub"); } ###################### # C3 Based Namespace # ###################### sub test_gryphon_object_methods_follow_c3_mro { my $animal = C3Animal::Animal->create(); my $lion = C3Animal::Lion->create(); my $eagle = C3Animal::Eagle->create(); my $gryphon = C3Animal::Gryphon->create(); is($lion->foo, $animal->foo, "Lion's foo is the same as Animal's"); isnt($eagle->foo, $animal->foo, "Eagle's foo is not the same as Animal's"); is($gryphon->foo, $eagle->foo, "Gryphon's foo is the same as Eagle's"); } sub test_gryphon_inheritance_follows_c3_mro { my $gryphon = C3Animal::Gryphon->create(); isa_ok($gryphon, 'C3Animal::Gryphon', '$gryphon isa C3Animal::Gryphon'); isa_ok($gryphon, 'C3Animal::Lion', '$gryphon isa C3Animal::Lion'); isa_ok($gryphon, 'C3Animal::Eagle', '$gryphon isa C3Animal::Eagle'); is(mro::get_mro('C3Animal::Gryphon'), 'c3', "Gryphon's MRO is C3"); my $i = 0; my $mro_linear_isa = mro::get_linear_isa('C3Animal::Gryphon'); my %inheritance = map { $_ => $i++ } @$mro_linear_isa; ok($inheritance{'C3Animal::Lion'} < $inheritance{'C3Animal::Eagle'}, 'Lion is higher precendence than Eagle'); ok($inheritance{'C3Animal::Eagle'} < $inheritance{'UR::Object'}, 'Eagle is higher precendence than UR::Object'); } sub test_class_property_follows_c3_mro { # This is theoretically the same check as comparing $gryphon->foo to $eagle->foo # However, it appears that property resolution is different than method resolution # since property resolution is done by hand and is probably a breadth first search. my $meta = UR::Object::Type->get(class_name => 'C3Animal::Gryphon'); my $foo_property_meta = $meta->property_meta_for_name('foo'); is($foo_property_meta->class_name, 'C3Animal::Eagle', "Gryphon is using Eagle's foo"); my $foo_property = $meta->property('foo'); is($foo_property->class_name, 'C3Animal::Eagle', "Gryphon is using Eagle's foo"); } sub test_package_sub_follows_c3_mro { is(C3Animal::Animal->species(), 'Animal', "Make sure we installed species sub in Animal"); is(C3Animal::Eagle->species(), 'Eagle', "Make sure we installed species sub in Eagle"); is(C3Animal::Gryphon->species(), 'Eagle', "Gryphon called Eagle's species sub"); } sub setup_test_env { no warnings 'once'; my $animal_namespace_type = UR::Object::Type->define( class_name => 'Animal', is => 'UR::Namespace', ); isa_ok($animal_namespace_type, 'UR::Object::Type', 'defined Animal namespace'); ####################### # DFS Based Namespace # ####################### my $dfs_animal_namespace_type = UR::Object::Type->define( class_name => 'DfsAnimal', is => 'UR::Namespace', has => [ method_resolution_order => { is => 'Text', default_value => 'dfs', }, ], ); isa_ok($dfs_animal_namespace_type, 'UR::Object::Type', 'defined DfsAnimal namespace'); my $dfs_animal_namespace = DfsAnimal->get(); isa_ok($dfs_animal_namespace, 'UR::Namespace', 'got DfsAnimal namespace'); is($dfs_animal_namespace->method_resolution_order, 'dfs', "DfsAnimal's MRO is DFS"); my $dfs_animal_type = UR::Object::Type->define( class_name => 'DfsAnimal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Animal'; ), }, ], ); isa_ok($dfs_animal_type, 'UR::Object::Type', 'defined Animal'); is($dfs_animal_type->namespace, 'DfsAnimal', 'DfsAnimal::Animal is in Animal namespace'); *DfsAnimal::Animal::species = sub { 'Animal' }; my $dfs_lion_type = UR::Object::Type->define( class_name => 'DfsAnimal::Lion', is => 'DfsAnimal::Animal', ); isa_ok($dfs_lion_type, 'UR::Object::Type', 'defined DfsAnimal::Lion'); my $dfs_eagle_type = UR::Object::Type->define( class_name => 'DfsAnimal::Eagle', is => 'DfsAnimal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Eagle'; ), }, ], ); isa_ok($dfs_eagle_type, 'UR::Object::Type', 'defined DfsAnimal::Eagle'); no warnings 'redefine'; *DfsAnimal::Eagle::species = sub { 'Eagle' }; use warnings 'redefine'; my $dfs_gryphon_type = UR::Object::Type->define( class_name => 'DfsAnimal::Gryphon', is => ['DfsAnimal::Lion', 'DfsAnimal::Eagle'], ); isa_ok($dfs_gryphon_type, 'UR::Object::Type', 'defined DfsAnimal::Gryphon'); ###################### # C3 Based Namespace # ###################### my $c3_animal_namespace_type = UR::Object::Type->define( class_name => 'C3Animal', is => 'UR::Namespace', has => [ method_resolution_order => { is => 'Text', default_value => 'c3', }, ], ); isa_ok($c3_animal_namespace_type, 'UR::Object::Type', 'defined C3Animal namespace'); my $c3_animal_namespace = C3Animal->get(); isa_ok($c3_animal_namespace, 'UR::Namespace', 'got C3Animal namespace'); is($c3_animal_namespace->method_resolution_order, 'c3', "C3Animal's MRO is C3"); my $c3_animal_type = UR::Object::Type->define( class_name => 'C3Animal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Animal'; ), }, ], ); isa_ok($c3_animal_type, 'UR::Object::Type', 'defined Animal'); is($c3_animal_type->namespace, 'C3Animal', 'C3Animal::Animal is in Animal namespace'); *C3Animal::Animal::species = sub { 'Animal' }; my $c3_lion_type = UR::Object::Type->define( class_name => 'C3Animal::Lion', is => 'C3Animal::Animal', ); isa_ok($c3_lion_type, 'UR::Object::Type', 'defined C3Animal::Lion'); my $c3_eagle_type = UR::Object::Type->define( class_name => 'C3Animal::Eagle', is => 'C3Animal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Eagle'; ), }, ], ); isa_ok($c3_eagle_type, 'UR::Object::Type', 'defined C3Animal::Eagle'); no warnings 'redefine'; *C3Animal::Eagle::species = sub { 'Eagle' }; use warnings 'redefine'; my $c3_gryphon_type = UR::Object::Type->define( class_name => 'C3Animal::Gryphon', is => ['C3Animal::Lion', 'C3Animal::Eagle'], ); isa_ok($c3_gryphon_type, 'UR::Object::Type', 'defined C3Animal::Gryphon'); use warnings 'once'; } resolve_param_value_from_cmdline_text.t100664023532023421 230412544604517 23017 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More tests => 3; my $some_object_meta = UR::Object::Type->define( class_name => 'SomeObject', has => [ name => { is => 'Text', }, ], ); my $some_command_meta = UR::Object::Type->define( class_name => 'SomeCommand', is => 'Command::V2', has => [ some_objects => { is => 'SomeObject', is_many => 1, require_user_verify => 0, }, ], ); for my $name (qw(Alice Bob Eve)) { SomeObject->create(name => $name); } my $pmeta = $some_command_meta->properties(property_name => 'some_objects'); my %test_queries = ( 'list of names specified by "in clause"' => [ q(name in ['Alice'), q('Bob']) ], 'list of names specified by colon' => [ q(name:Alice/Bob) ], 'list of names' => [ q(Alice), q(Bob) ], ); for my $test (keys %test_queries) { my $value = $test_queries{$test}; my @o = SomeCommand->resolve_param_value_from_cmdline_text({ name => $pmeta->property_name, class => $pmeta->data_type, value => $value, }); is(scalar(@o), 2, $test); } url-router.t100664023532023421 1012512544604517 17165 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/services#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use UR; use strict; use warnings; use Plack::HTTPParser; use Plack::Util; use HTTP::Request; plan tests => 18; my $router = UR::Service::UrlRouter->create(); ok($router, 'Created a UrlRouter'); # Basic string routes foreach my $method ( qw(GET POST PUT DELETE) ) { $router->$method('/thing', sub { return "$method Some Content" }); } $router->GET('/stuff', sub { return [ 200, [ Header => 'Value'], [ 'Stuff Content' ]] }); my $fourohfour = [ 404, [ 'Content-Type' => 'text/plain' ], [ 'Not Found' ] ]; my $resp = $router->( make_psgi_env('GET', '/nomatch') ); is_deeply($resp, $fourohfour, 'GET non-matching path returns 404'); $resp = $router->( make_psgi_env('GET', '/thing') ); is_deeply($resp, [ 200, [], ['GET Some Content'] ], 'Run route for GET /thing'); $resp = $router->( make_psgi_env('POST', '/thing') ); is_deeply($resp, [ 200, [], ['POST Some Content'] ], 'Run route for POST /thing'); $resp = $router->( make_psgi_env('PUT', '/thing') ); is_deeply($resp, [ 200, [], ['PUT Some Content'] ], 'Run route for PUT /thing'); $resp = $router->( make_psgi_env('DELETE', '/thing') ); is_deeply($resp, [ 200, [], ['DELETE Some Content'] ], 'Run route for DELETE /thing'); $resp = $router->( make_psgi_env('GET', '/stuff') ); is_deeply($resp, [ 200, [ Header => 'Value'], ['Stuff Content']], 'Run route that returns PSGI struct'); # A subref route that fires if the first char is a T $router = UR::Service::UrlRouter->create(); ok($router, 'Created UrlRouter'); $router->GET(sub { return substr(shift->{PATH_INFO}, 0, 1) eq 'T' }, sub { return "Started with T" }); $resp = $router->( make_psgi_env('GET', 'Tfoo') ); is_deeply($resp, [ 200, [], [ 'Started with T' ] ], 'Match route with subref'); $resp = $router->( make_psgi_env('PUT', 'Tfoo') ); is_deeply($resp, $fourohfour, 'Did not match subref route with different method'); $resp = $router->( make_psgi_env('GET', 'tfoo') ); is_deeply($resp, $fourohfour, 'Did not match subref route with non-matching string'); # A regex route $router = UR::Service::UrlRouter->create(); ok($router, 'Created UrlRouter'); my @matches; $router->GET(qr{^\/(\w+)\/(\w+)}, sub { my $env = shift; @matches = @_; return 1; }); $resp = $router->( make_psgi_env('GET', '/one/two')); is_deeply($resp, [200, [], [1]], 'Run route matching regex'); is_deeply(\@matches, ['one','two'], 'Callback saw the matches'); $resp = $router->( make_psgi_env('GET', '/one/two/three/four') ); is_deeply($resp, [200, [], [1]], 'Run route matching regex'); is_deeply(\@matches, ['one','two'], 'Callback saw the matches'); $resp = $router->( make_psgi_env('GET', 'one/two') ); is_deeply($resp, $fourohfour, 'Did not match regex with non-matching path'); $resp = $router->( make_psgi_env('PUT', '/one/two')); is_deeply($resp, $fourohfour, 'Did not match regex with different method'); sub make_psgi_env { my $method = shift; my $path = shift; my $req = HTTP::Request->new($method, $path); $req->protocol('HTTP/1.1'); # copied from HTTP::Server::PSGI::accept_loop() my $env = { SERVER_PORT => 80, SERVER_NAME => 'localhost', SCRIPT_NAME => '', REMOTE_ADDR => 'localhost', REMOTE_PORT => $$, 'psgi.version' => [ 1, 1 ], 'psgi.errors' => *STDERR, 'psgi.url_scheme' => 'http', 'psgi.run_once' => Plack::Util::FALSE, 'psgi.multithread' => Plack::Util::FALSE, 'psgi.multiprocess' => Plack::Util::FALSE, 'psgi.streaming' => Plack::Util::TRUE, 'psgi.nonblocking' => Plack::Util::FALSE, 'psgix.input.buffered' => Plack::Util::TRUE, }; Plack::HTTPParser::parse_http_request($req->as_string, $env); return $env; } webserver.t100664023532023421 1676612544604517 17072 0ustar00abrummetgsc000000000000UR-0.44/t/URT/t/services#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use UR; use strict; use warnings; use IO::Socket; use HTTP::Request; use File::Temp; use Plack::Util; plan tests => 43; # Test out the lazy host and port methods { my $srv = UR::Service::WebServer->create(); ok($srv, 'Created WebServer service'); $srv->dump_status_messages(0); $srv->queue_status_messages(1); is($srv->port(12345), 12345, 'Can change port before socket is created'); is($srv->port(undef), undef, 'Change port back to undef'); is($srv->host('abc'), 'abc', 'Can change host before socket is created'); is($srv->host(undef), undef, 'Change host back to undef'); my $port = $srv->port; ok($port, 'Forced port to be filled in'); my $host = $srv->host; my $server = $srv->server; is($port, $server->listen_sock->sockport, "autogenerated port matches server's sockport"); is($host, $server->listen_sock->sockhost, "autogenerated port matches server's sockhost"); ok( !eval { $srv->port($port + 1) }, 'Setting port after socket creation fails'); like($@, qr(Cannot change port), 'Exception looks correct'); ok( !eval { $srv->host($host .'aa') }, 'Setting host after socket creation fails'); like($@, qr(Cannot change host), 'Exception looks correct'); } # test out making a socket with a random port and connecting to it { my $srv = UR::Service::WebServer->create(); ok($srv, 'Created WebServer service'); $srv->dump_status_messages(0); $srv->queue_status_messages(1); my $server = $srv->server(); isa_ok($server, 'UR::Service::WebServer::Server'); ok($server->setup_listener, 'setup_listener'); my $sock = $server->listen_sock; my $port = $sock->sockport; ok($port, "server is listening on random port: $port"); my $host = $sock->sockhost; is($host, '127.0.0.1', 'Default listening on localhost'); # try connecting my $conn = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port, Proto => 'tcp'); ok($conn, 'Connected'); $conn->close(); ok($srv->delete(), 'Delete WebServer'); # try connecting again $conn = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port, Proto => 'tcp'); ok(!$conn, "Connection to deleted WebServer failed: $!"); } # Make another and specify the listen port { my $tryport = pick_random_port(); my $srv = UR::Service::WebServer->create(port => $tryport); ok($srv, 'Create WebServer service specifying port'); $srv->dump_status_messages(0); $srv->queue_status_messages(1); ok($srv->server->setup_listener, 'setup_listener'); my $sock = $srv->server->listen_sock; my $port = $sock->sockport; is($port, $tryport, 'Listen port is correct'); # try connecting my $conn = IO::Socket::INET->new(PeerAddr => 'localhost', PeerPort => $tryport, Proto => 'tcp'); ok($conn, 'Connected'); $conn->close(); $srv->delete(); } # Test the idle timeout { my $srv = UR::Service::WebServer->create(idle_timeout => 1); ok($srv, 'Created WebServer service'); $srv->dump_status_messages(0); $srv->queue_status_messages(1); # NOTE - This will hang if the test fails :( $srv->run(sub {} ); ok(1, 'timeout'); $srv->delete(); } # Test running through a connection and request { #my $srv = UR::Service::WebServer->create(idle_timeout => 1); my $srv = UR::Service::WebServer->create(); ok($srv, 'Create WebServer service'); $srv->dump_status_messages(0); $srv->queue_status_messages(1); my($req_method, $path_info); my $handler = sub { my $env = shift; ($req_method, $path_info) = @$env{'REQUEST_METHOD','PATH_INFO'}; # HACK since old versions of HTTP::Server::PSGI don't # support psgix.harakiri. We can't just simply die because the # handlers are wrapped inside an eval, but the response callbacks # are not. return sub { die "Worked\n" }; }; my $port = $srv->port; my $conn = IO::Socket::INET->new(PeerPort => $port, PeerHost => 'localhost', Proto => 'tcp'); my $req = HTTP::Request->new(GET => 'http://localhost/test', [ 'user-agent' => 'Test 1.0'] ); $req->protocol('HTTP/1.1'); $srv->server->buffer_input($req->as_string); eval { $srv->run($handler) }; is($@, "Worked\n", 'Request handler was invoked'); is($req_method, 'GET', 'Request method matches request'); is($path_info, 'http://localhost/test', 'Request path matches request'); $conn->close; # Test assigning a callback before run() $conn = IO::Socket::INET->new(PeerPort => $port, PeerHost => 'localhost', Proto => 'tcp'); $srv->cb($handler); $srv->server->buffer_input($req->as_string); eval { $srv->run() }; is($@, "Worked\n", 'Request handler was invoked'); is($req_method, 'GET', 'Request method matches request'); is($path_info, 'http://localhost/test', 'Request path matches request'); } # Test the file/directory handling, non streaming and streaming { for my $does_streaming ( Plack::Util::FALSE, Plack::Util::TRUE) { my $check_dirhandler_result = sub { my($data, $expected_data, $message) = @_; if ($does_streaming and ref($data->[2]) ne 'ARRAY') { my $fh = $data->[2]; local $/; $data->[2] = [ <$fh> ]; } is_deeply($data, $expected_data, $message); }; my $tmpdir = File::Temp::tempdir( CLEANUP => 1 ); IO::File->new($tmpdir . '/file1','w')->print("This is file 1\n"); IO::File->new($tmpdir . '/file2.css','w')->print("This is file 2\n"); IO::File->new($tmpdir . '/file3.html','w')->print("This is file 3\n"); my $dirhandler = UR::Service::WebServer->file_handler_for_directory( $tmpdir, $does_streaming); my $psgi_env = { 'psgi.streaming' => $does_streaming }; ok($dirhandler, 'Create file handler for directory'); my $data = $dirhandler->($psgi_env, '/file1'); $check_dirhandler_result->( $data, [ 200, [ 'Content-Type' => 'text/plain' ], ["This is file 1\n"]], 'Got data for file1' ); $data = $dirhandler->($psgi_env, '/nothere'); $check_dirhandler_result->( $data, [ 404, [ 'Content-Type' => 'text/plain' ], ["Not Found"]], 'Got 404 for non-existent file' ); $data = $dirhandler->($psgi_env, '/file2.css'); $check_dirhandler_result->( $data, [ 200, [ 'Content-Type' => 'text/css' ], ["This is file 2\n"]], 'Got data for file2' ); $data = $dirhandler->($psgi_env, '/file3.html'); $check_dirhandler_result->( $data, [ 200, [ 'Content-Type' => 'text/html' ], ["This is file 3\n"]], 'Got data for file3' ); } } sub pick_random_port { # Have the system make socket and pick the port for us my $trysock = IO::Socket::INET->new(Listen => 1, Proto => 'tcp', LocalAddr => 'localhost'); my $tryport = $trysock->sockport; # Now close this socket, and tell the WebServer to use the same socket # There's a small chance of a race condition here if another process # re-uses the same port number, but the chance is _really_ small, since the # port is in TIME_WAIT. The underlying socket open in the webserver code # uses SO_REUSEADDR to re-open the port. $trysock->close(); return $tryport; } ur_data_type_for_data_source_data_type.t100664023532023421 107512544604517 23137 0ustar00abrummetgsc000000000000UR-0.44/t/URT/tuse strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $uc_dt = URT::DataSource::SomeOracle->ur_data_type_for_data_source_data_type('TIMESTAMP'); my $ps_dt = URT::DataSource::SomeOracle->ur_data_type_for_data_source_data_type('TIMESTAMP(9)'); my $lc_dt = URT::DataSource::SomeOracle->ur_data_type_for_data_source_data_type('timestamp'); is($ps_dt, $uc_dt, 'data type with paren suffix matches upper case result'); is($lc_dt, $uc_dt, 'lower case data type matches upper case result'); Vending.pm100664023532023421 27412544604517 14014 0ustar00abrummetgsc000000000000UR-0.44/tpackage Vending; use warnings; use strict; use UR; class Vending { is => [ 'UR::Namespace' ], has_constant => [ allow_sloppy_primitives => { value => 1 }, ] }; 1; Coin.pm100664023532023421 130612544604517 14721 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Coin; use strict; use warnings; use Vending; class Vending::Coin { table_name => 'COIN', is => 'Vending::Content', id_by => [ coin_id => { is => 'integer' }, ], has => [ name => { via => 'item_type', to => 'name' }, value_cents => { via => 'coin_type', to => 'value_cents' }, coin_type => { is => 'Vending::CoinType', id_by => 'name' }, item_type => { is => 'Vending::ContentType', id_by => 'type_id', constraint_name => 'COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; sub subtype_name_resolver { return __PACKAGE__; } 1; CoinType.pm100664023532023421 130712544604517 15564 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::CoinType; use strict; use warnings; use Vending; class Vending::CoinType { table_name => 'coin_type', id_by => [ name => { is => 'String' }, ], has => [ value_cents => { is => 'integer' }, item_type => { is => 'Vending::ItemType', where => [ name => \'name'] }, type_id => { via => 'item_type' }, ], doc => 'kinds of coins the machine accepts, and their value', data_source => 'Vending::DataSource::CoinType', }; # Overriding because the property definition doesn't exactly work... sub item_type { my $self = shift; my $type_obj = Vending::ContentType->get_or_create(name => $self->name); return $type_obj; } 1; Command.pm100664023532023421 53412544604517 15371 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Command; use strict; use warnings; use Vending; class Vending::Command { is => 'Command', has => [ machine_id => { default_value => 1 }, machine => { is => 'Vending::Machine', id_by => 'machine_id' }, ], }; #sub machine { # my $machine = Vending::Machine->get(); # return $machine; #} 1; Buy.pm100664023532023421 124112544604517 16144 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Buy; use strict; use warnings; use Vending; class Vending::Command::Buy { is => 'Vending::Command::Outputter', doc => 'Attempt to get a sellable item', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] }; sub help_detail { q(Buy an item from one of the vending machine's slots. Command line argument is one of the slot/button names); } sub _get_items_to_output { my $self = shift; my $slot_names = [$self->bare_args]; my $machine = $self->machine; my @bought = $machine->buy(@$slot_names); return @bought; } 1; CoinReturn.pm100664023532023421 55012544604517 17457 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::CoinReturn; use strict; use warnings; class Vending::Command::CoinReturn { is => 'Vending::Command::Outputter', doc => 'Return all inserted coins back to the customer', }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @items = $machine->coin_return(); return @items; } 1; Dime.pm100664023532023421 40512544604517 16244 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Dime; use strict; use warnings; class Vending::Command::Dime { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'dime' }, ], doc => 'Insert a dime into the machine', }; 1; Dollar.pm100664023532023421 41512544604517 16604 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Dollar; use strict; use warnings; class Vending::Command::Dollar { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'dollar' }, ], doc => 'Insert a dollar into the machine', }; 1; InsertMoney.pm100664023532023421 74012544604517 17644 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::InsertMoney; use strict; use warnings; class Vending::Command::InsertMoney { is => 'Vending::Command', doc => 'Base abstract class for the money inserting commands', #is_abstract => 1, has => [ name => { is => 'String'}, #, is_abstract => 1 }, ] }; sub execute { my $self = shift; my $name = $self->name(); my $machine = $self->machine(); my $worked = $machine->insert($name); return $worked; } 1; Menu.pm100664023532023421 145212544604517 16315 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Menu; use strict; use warnings; class Vending::Command::Menu { is => ['UR::Object::Command::List', 'Vending::Command' ], doc => 'Show the items available to buy', has => [ subject_class_name => { is_constant => 1, value => 'Vending::MachineLocation' }, filter => { value => 'is_buyable=1' }, show => { value => 'name,label,price' }, ], }; sub execute { my $self = shift; my $super = $self->super_can('_execute_body'); $super->($self,@_); #$DB::single=1; my $machine = $self->machine; my $inserted = $machine->coin_box->content_value(); if ($inserted) { printf("You have inserted \$%.2f so far\n", $inserted/100); } else { print "You have not inserted any money yet\n"; } return 1; } 1; Nickel.pm100664023532023421 41512544604517 16574 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Nickel; use strict; use warnings; class Vending::Command::Nickel { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'nickel' }, ], doc => 'Insert a Nickel into the machine', }; 1; Outputter.pm100664023532023421 66212544604517 17406 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Outputter; use strict; use warnings; class Vending::Command::Outputter { is_abstract => 1, is => 'Vending::Command', doc => 'Abstract parent class for things that output items to the user', }; sub execute { my $self = shift; my @user_items = $self->_get_items_to_output(); foreach my $item ( @user_items ) { print "You get: ",$item->name,"\n"; } return 1; } Quarter.pm100664023532023421 42112544604517 17007 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Quarter; use strict; use warnings; class Vending::Command::Quarter { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'quarter' }, ], doc => 'Insert a quarter into the machine', }; 1; Service.pm100664023532023421 27512544604517 16773 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Commandpackage Vending::Command::Service; use strict; use warnings; use Vending; class Vending::Command::Service { is => 'Vending::Command', doc => 'Service-mode commands', }; 1; Add.pm100664023532023421 35312544604517 17460 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Servicepackage Vending::Command::Service::Add; use strict; use warnings; use Vending; class Vending::Command::Service::Add { is => 'Vending::Command::Service', doc => 'Add items to the vending machine', is_abstract => 1, }; 1; Change.pm100664023532023421 176212544604517 20672 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Change; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Change { is => 'Vending::Command::Service::Add', doc => 'Add change to the vending machine', has => [ name => { is => 'String', doc => 'Name of the coin' }, count => { is => 'Integer', doc => 'How many you are adding' }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $coin_kind = Vending::CoinType->get(name => $self->name); unless ($coin_kind) { $self->error_message($self->name." is not a valid coin name"); return; } my $change_disp = $machine->change_dispenser; unless ($change_disp) { die "Couldn't retrieve money location for 'change'"; } my $count = $self->count; while($count--) { my $coin = $change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $coin_kind->type_id, machine_id => $self); 1; } return 1; } 1; Inventory.pm100664023532023421 344312544604517 21500 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Inventory; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Inventory { is => 'Vending::Command::Service::Add', doc => 'Add a sellable item to the vending machine', has => [ slot => { is => 'String', doc => 'Slot name you are putting the product into' }, name => { is => 'String', doc => 'Name of the item, default is the label on the slot', is_optional => 1 }, count => { is => 'Integer', doc => 'How many you are adding, default is 1', default_value => 1 }, ], }; sub help_detail { q(Add inventory to the machine in the given slot. You are allowed to put items into a slot that do not necessarily match the slot's label name. Example: vend service add inventory --slot a --name Cookie --count 4 ); } sub execute { my $self = shift; my $machine = $self->machine; my $loc = $machine->machine_locations(name => $self->slot); unless ($loc) { die "There is no slot with that name"; } unless (defined $self->name) { print "Adding ",$loc->label,"(s)\n"; $self->name($loc->label); } my $item_kind = $machine->products(name => $self->name); unless ($item_kind) { print "This is a new item. What is the manufacturer:\n"; my $manufacturer = ; print "What is the cost (dollars)\n"; my $price = ; $price = int($price * 100); # Convert to cents $item_kind = $machine->add_product(name => $self->name, manufacturer => $manufacturer, cost_cents => $price); } my $count = $self->count; while($count--) { my $item = $loc->add_item(subtype_name => 'Vending::Merchandise', product_id => $item_kind->id, insert_date => time(), machine_id => $self); } return 1; } 1; Slot.pm100664023532023421 151712544604517 20424 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Slot; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Slot { is => 'Vending::Command::Service::Add', doc => 'Install a new vending slot into the machine', has => [ name => { is => 'String', doc => 'Button name for the slot' }, label => { is => 'String', doc => 'Display label for this slot' }, cost => { is => 'Integer', doc => 'Price for this slot, in cents' }, ], }; sub execute { my $self = shift; my $machine = $self->machine; my $slot = $machine->add_machine_location(name => $self->name, label => $self->label, cost_cents => $self->cost, is_buyable => 1); return 1; } 1; ConfigureSlot.pm100664023532023421 153012544604517 21571 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Servicepackage Vending::Command::Service::ConfigureSlot; use strict; use warnings; use Vending; class Vending::Command::Service::ConfigureSlot { is => 'Vending::Command::Service', has => [ name => { is => 'String', doc => 'Slot name' }, label => { is => 'String', doc => 'New label for the slot', is_optional => 1 }, cost_cents => { is => 'String', doc => 'New price for this slot', is_optional => 1 }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $loc = $machine->machine_locations(name => $self->name); unless ($loc) { $self->error_message("Not a valid slot name"); return; } if (defined $self->label) { $loc->label($self->label); } if (defined $self->cost_cents) { $loc->cost_cents($self->cost_cents); } return 1; } 1; EmptyBank.pm100664023532023421 62612544604517 20665 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Servicepackage Vending::Command::Service::EmptyBank; use strict; use warnings; use Vending; class Vending::Command::Service::EmptyBank { is => ['Vending::Command::Outputter', 'Vending::Command::Service'], doc => 'Get all the money out of the bank', }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @coins = $machine->empty_bank(); return @coins; } 1; RemoveSlot.pm100664023532023421 117512544604517 21112 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Servicepackage Vending::Command::Service::RemoveSlot; use strict; use warnings; use Vending; class Vending::Command::Service::RemoveSlot { is => ['Vending::Command::Outputter', 'Vending::Command::Service'], doc => 'Uninstall the named slot and remove all the items', has => [ name => { is => 'String', doc => 'Name of the slot to empty out' }, ], }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @items = $machine->empty_machine_location_by_name($self->name); my $loc = $machine->machine_locations(name => $self->name); $loc->delete; return @items; } 1; Show.pm100664023532023421 34612544604517 17712 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Servicepackage Vending::Command::Service::Show; use strict; use warnings; use Vending; class Vending::Command::Service::Show { is => 'Vending::Command::Service', doc => 'Various sub-commands to query the machine state', }; 1; Bank.pm100664023532023421 50012544604517 20555 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Bank; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Bank { is => 'Vending::Command::Service::Show::Money', doc => "Show how much money is in the machine's bank", has => [ location_name => { value => 'bank' }, ], }; 1; Change.pm100664023532023421 52212544604517 21073 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Change; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Change { is => 'Vending::Command::Service::Show::Money', doc => "Show how much money is in the machine's change dispenser", has => [ location_name => { value => 'change' }, ], }; 1; Inventory.pm100664023532023421 167512544604517 21735 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Inventory; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Inventory { is => [ 'UR::Object::Command::List', 'Vending::Command::Service'], has => [ subject_class_name => { value => 'Vending::Merchandise' }, show => { value => 'id,location_name,name,insert_date' }, filter => { is_calculated => 1 }, bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ], }; sub filter { my $self = shift; my $slot_names = [$self->bare_args]; #$DB::single=1; my $filter = 'machine_id='.$self->machine_id; if (@$slot_names == 1) { $filter = 'slot_name='.$slot_names->[0]; } elsif (@$slot_names) { $filter = 'slot_name=:'.join('/',@$slot_names); } return $filter; } sub execute { #$DB::single = 1; shift->SUPER::_execute_body(@_) } 1; Money.pm100664023532023421 171212544604517 21017 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Money; use strict; use warnings; class Vending::Command::Service::Show::Money { is_abstract => 1, is => 'Vending::Command::Service', doc => 'parent class for show change and show bank', has => [ location_name => { is => 'String', is_abstract => 1 }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $loc = $machine->machine_locations(name => $self->location_name); unless ($loc) { $self->error_message("There is no slot named ".$self->location_name); return; } my @coins = $loc->items; my %coins_by_type; my $total_value = 0; foreach my $coin ( @coins ) { $coins_by_type{$coin->name}++; $total_value += $coin->value_cents; } while(my($type,$count) = each %coins_by_type) { printf("%-7s:%6d\n", $type,$count); } printf("Total:\t\$%.2f\n",$total_value/100); return 1; } 1; Slots.pm100664023532023421 65012544604517 21014 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Slots; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Slots { is => 'UR::Object::Command::List', has => [ subject_class_name => { value => 'Vending::MachineLocation' }, show => { value => 'name,label,price,count,content_value_dollars' }, ], doc => "Display information about what is in the machine's slots", }; 1; Content.pm100664023532023421 330012544604517 15437 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Content; use strict; use warnings; use Vending; class Vending::Content { table_name => 'CONTENT', is_abstract => 1, subclassify_by => 'subtype_name', id_by => [ content_id => { }, ], has => [ machine => { is => 'Vending::Machine', id_by => 'machine_id', constraint_name => 'CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK' }, machine_id => { value => '1', is_constant => 1, is_classwide => 1, column_name => '' }, machine_location_id => { is => 'integer' }, subtype_name => { is => 'varchar', is_optional => 1 }, machine_location => { is => 'Vending::MachineLocation', id_by => 'machine_location_id', constraint_name => 'CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK' }, location_name => { via => 'machine_location', to => 'name' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; # Called when you try to create a generic Vending::Content sub subtype_name_resolver { my $class = shift; my %params; if (ref($_[0])) { %params = %{$_[0]}; # Called with obj as arg } else { %params = @_; # called with hash as arglist } return $params{'subtype_name'}; } # Turn this thing into a Vending::ReturnedItem to give back to the user # as a side effect, $self is deleted sub dispense { my $self = shift; my @items_to_dispense; if (ref($self)) { # object method... @items_to_dispense = ($self); } else { # Class method @items_to_dispense = @_; } return Vending::ReturnedItem->create_from_vend_items(@items_to_dispense); } 1; ContentType.pm100664023532023421 231112544604517 16302 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::ContentType; use strict; use warnings; use Vending; class Vending::ContentType { table_name => 'content_type', id_by => [ type_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, machine_id => { value => '1', is_constant => 1, is_classwide => 1, column_name => '' }, machine => { is => 'Vending::Machine', id_by => 'machine_id' }, count => { calculate_from => ['type_id'], calculate => \&count_items_by_type, doc => 'How many items of this type are there' }, ], id_sequence_generator_name => 'URMETA_content_type_TYPE_ID_seq', doc => 'abstract base class for things the machine knows about', schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; sub count_items_by_type { my $type_id = shift; my $item = Vending::CoinType->get($type_id) || Vending::Product->get($type_id); my @objects; if ($item->isa('Vending::CoinType')) { @objects = Vending::Coin->get(type_id => $type_id); } else { @objects = Vending::Merchandise->get(product_id => $type_id); } return scalar(@objects); } 1; CoinType.pm100664023532023421 72612544604517 17602 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourcepackage Vending::DataSource::CoinType; use strict; use warnings; use Vending; my $path = Vending->get_base_directory_name() . '/DataSource/coin_types.tsv'; class Vending::DataSource::CoinType { is => ['UR::DataSource::File','UR::Singleton'], has_constant => [ server => { value => $path }, delimiter => { value => '\s+' }, column_order => { value => ['name','value_cents'] }, sort_order => { value => ['name'] }, ], }; 1; Machine.pm100664023532023421 205212544604517 17426 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourcepackage Vending::DataSource::Machine; use strict; use warnings; use Vending; class Vending::DataSource::Machine { is => [ 'UR::DataSource::SQLite', 'UR::Singleton' ], }; use File::Temp; sub server { our $FILE; unless ($FILE) { (undef, $FILE) = File::Temp::tempfile('ur_testsuite_vend_XXXX', OPEN => 0, UNKINK => 0, TMPDIR => 1, SUFFIX => '.sqlite3'); } return $FILE; } # Don't print warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::Meta. sub _dont_emit_initializing_messages { my($dsobj, $msg) = @_; if ($msg =~ m/^Re-creating/) { $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { our $FILE; unlink $FILE; } 1; Machine.sqlite3-dump100664023532023421 651512544604517 21351 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourceBEGIN TRANSACTION; CREATE TABLE machine (machine_id Integer PRIMARY KEY NOT NULL, address Text); INSERT INTO "machine" VALUES(1,'127.0.0.1'); CREATE TABLE content_type (type_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL); INSERT INTO "content_type" VALUES(1,'dollar'); INSERT INTO "content_type" VALUES(2,'quarter'); INSERT INTO "content_type" VALUES(3,'dime'); INSERT INTO "content_type" VALUES(4,'nickel'); INSERT INTO "content_type" VALUES(9,'shilling'); INSERT INTO "content_type" VALUES(11,'diamond'); CREATE TABLE coin (coin_id integer PRIMARY KEY NOT NULL REFERENCES content(content_id), type_id integer NOT NULL REFERENCES content_type(type_id)); INSERT INTO "coin" VALUES(130,1); INSERT INTO "coin" VALUES(131,1); INSERT INTO "coin" VALUES(132,1); INSERT INTO "coin" VALUES(133,1); INSERT INTO "coin" VALUES(134,1); INSERT INTO "coin" VALUES(135,1); CREATE TABLE product (product_id integer PRIMARY KEY NOT NULL REFERENCES content_type(type_id), cost_cents integer NOT NULL, manufacturer varchar NOT NULL); CREATE TABLE merchandise (merchandise_id integer PRIMARY KEY NOT NULL, product_id integer NOT NULL REFERENCES product(product_id), insert_date datetime NOT NULL DEFAULT (date('now'))); CREATE TABLE URMETA_machine_location_SLOT_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); DELETE FROM sqlite_sequence; INSERT INTO "sqlite_sequence" VALUES('URMETA_machine_location_SLOT_ID_seq',14); INSERT INTO "sqlite_sequence" VALUES('URMETA_content_type_TYPE_ID_seq',11); INSERT INTO "sqlite_sequence" VALUES('URMETA_COIN_COIN_ID_seq',135); CREATE TABLE machine_location (machine_location_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, is_buyable integer NOT NULL, cost_cents integer, label varchar, machine_id integer NOT NULL REFERENCES machine(machine_id)); INSERT INTO "machine_location" VALUES(8,'a',1,65,'Cookie',1); INSERT INTO "machine_location" VALUES(9,'b',1,100,'Apple',1); INSERT INTO "machine_location" VALUES(10,'c',1,150,'Coke',1); INSERT INTO "machine_location" VALUES(11,'bank',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(12,'box',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(13,'change',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(14,'d',1,10000,'iPod',1); CREATE TABLE content (content_id PRIMARY KEY NOT NULL, subtype_name varchar, machine_location_id integer NOT NULL REFERENCES machine_location(machine_location_id), machine_id NOT NULL REFERENCES machine(machine_id)); INSERT INTO "content" VALUES('129','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('130','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('131','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('132','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('133','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('134','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('135','Vending::Coin',12,'1'); CREATE TABLE URMETA_content_type_TYPE_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); CREATE TABLE URMETA_COIN_COIN_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); COMMIT; Meta.pm100664023532023421 126112544604517 16751 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourcepackage Vending::DataSource::Meta; use warnings; use strict; use UR; class Vending::DataSource::Meta { is => [ 'UR::DataSource::Meta' ], }; # Don't print out warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::SomeSQLite sub _dont_emit_initializing_messages { my($dsobj, $message) = @_; if ($message =~ m/^Re-creating/) { # don't emit the message about re-creating the DB when run in the test harness $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } 1; Meta.sqlite3-dump100664023532023421 2263212544604517 20711 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','MERCHANDISE','PRODUCT','MERCHANDISE_PRODUCT_ID_PRODUCT_PRODUCT_ID_FK','2009-05-07 14:09:28'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','MACHINE_LOCATION','MACHINE','MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK','2009-05-07 14:09:27'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','CONTENT','MACHINE_LOCATION','CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK','2009-05-07 14:09:27'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','CONTENT','MACHINE','CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK','2009-05-07 14:09:29'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','PRODUCT','CONTENT_TYPE','PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK','2009-05-07 14:09:28'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','COIN','CONTENT','COIN_COIN_ID_CONTENT_CONTENT_ID_FK','2009-05-07 14:09:29'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','COIN','CONTENT_TYPE','COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK','2009-05-07 14:09:29'); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); INSERT INTO "dd_fk_constraint_column" VALUES('COIN_COIN_ID_CONTENT_CONTENT_ID_FK','Vending::DataSource::Machine','main','COIN','CONTENT','COIN_ID','CONTENT_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK','Vending::DataSource::Machine','main','COIN','CONTENT_TYPE','TYPE_ID','TYPE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('MERCHANDISE_PRODUCT_ID_PRODUCT_PRODUCT_ID_FK','Vending::DataSource::Machine','main','MERCHANDISE','PRODUCT','PRODUCT_ID','PRODUCT_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK','Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE','MACHINE_ID','MACHINE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK','Vending::DataSource::Machine','main','CONTENT','MACHINE_LOCATION','MACHINE_LOCATION_ID','MACHINE_LOCATION_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK','Vending::DataSource::Machine','main','CONTENT','MACHINE','MACHINE_ID','MACHINE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK','Vending::DataSource::Machine','main','PRODUCT','CONTENT_TYPE','PRODUCT_ID','TYPE_ID'); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','CONTENT','content_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MACHINE','machine_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','type_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','product_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','COIN','coin_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','merchandise_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','machine_location_id',1); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MACHINE','TABLE','entity',NULL,'2009-05-07 14:09:29',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','TABLE','entity',NULL,'2009-05-07 14:09:27',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','TABLE','entity',NULL,'2009-05-07 14:09:30',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','COIN','TABLE','bridge',NULL,'2009-05-07 14:09:29',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','PRODUCT','TABLE','entity',NULL,'2009-05-07 14:09:28',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','CONTENT','TABLE','entity',NULL,'2009-05-07 14:09:30',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','TABLE','entity',NULL,'2009-05-07 14:09:29',NULL); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','PRODUCT_ID','integer',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','NAME','varchar',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','MANUFACTURER','varchar',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','COST_CENTS','integer',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE','MACHINE_ID','Integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','COIN','TYPE_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE_LOCATION_ID','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','CONTENT_ID','',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE','ADDRESS','Text',NULL,'Y','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','IS_BUYABLE','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','LABEL','varchar',NULL,'Y','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','TYPE_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','INSERT_DATE','datetime',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','SUBTYPE_NAME','varchar',NULL,'Y','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','PRODUCT_ID','integer',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','NAME','varchar',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE_ID','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','MACHINE_ID','',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','COST_CENTS','integer',NULL,'Y','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','COIN','COIN_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','MACHINE_LOCATION_ID','integer',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','MERCHANDISE_ID','integer',NULL,'N','2009-05-07 14:09:30',''); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; coin_types.tsv100664023532023421 6512544604517 20400 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/DataSourcediamond 10000 dime 10 dollar 100 nickel 5 quarter 25 Machine.pm100664023532023421 1315212544604517 15417 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Machine; use strict; use warnings; use Vending; class Vending::Machine { table_name => 'MACHINE', id_by => [ machine_id => { is => 'Integer' }, ], has => [ coin_box => { via => 'machine_locations', to => '-filter', where => [ name => 'box' ] }, bank => { via => 'machine_locations', to => '-filter', where => [ name => 'bank' ] }, change_dispenser => { via => 'machine_locations', to => '-filter', where => [ name => 'change' ] }, address => { is => 'Text', is_optional => 1 }, ], has_many => [ products => { is => 'Vending::Product', reverse_as => 'machine' }, items => { is => 'Vending::Content', reverse_as => 'machine' }, inventory_items => { is => 'Vending::Merchandise', reverse_as => 'machine' }, item_types => { is => 'Vending::ContentType', reverse_as => 'machine' }, machine_locations => { is => 'Vending::MachineLocation', reverse_as => 'machine' }, ], data_source => 'Vending::DataSource::Machine', }; sub insert { my($self, $item_name) = @_; my $coin_type = Vending::CoinType->get(name => $item_name); unless ($coin_type) { $self->error_message("This machine does not accept '$item_name' coins"); return; } my $loc = $self->coin_box(); my $coin = $loc->add_coin(type_id => $coin_type->type_id, machine_id => $self); return defined($coin); } sub coin_return { my $self = shift; my $loc = $self->coin_box; my @coins = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@coins); return @returned_items; } sub empty_bank { my $self = shift; my $loc = $self->bank(); my @coins = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@coins); return @returned_items; } sub empty_machine_location_by_name { my($self,$name) = @_; my $loc = $self->machine_locations(name => $name); return unless $loc; unless ($loc->is_buyable) { die "You can only empty out inventory type machine_locations"; } my @items = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@items); return @returned_items; } sub buy { my($self,@machine_location_names) = @_; my $coin_box = $self->coin_box(); my $transaction = UR::Context::Transaction->begin(); my @returned_items = eval { my $users_money = $coin_box->content_value(); my @bought_items; my %iterator_for_machine_location; foreach my $loc_name ( @machine_location_names ) { my $machine_location = $self->machine_locations(name => $loc_name); unless ($machine_location && $machine_location->is_buyable) { die "$loc_name is not a valid choice\n"; } my $iter = $iterator_for_machine_location{$loc_name} || $machine_location->item_iterator(); unless ($iter) { die "Problem creating iterator for $loc_name\n"; return; } my $item = $iter->next(); # This is the one they'll buy unless ($item) { $self->error_message("Item $loc_name is empty"); next; } push @bought_items, $item->dispense; } my @change; if (@bought_items) { @change = $self->_complete_purchase_and_make_change_for_selections(@bought_items); } return (@change,@bought_items); }; if ($@) { my($error) = ($@ =~ m/^(.*?)\n/); $self->error_message("Couldn't process your purchase:\n$error"); $transaction->rollback(); return; } else { $transaction->commit(); return @returned_items; } } # Note that this will die if there's a problem making change sub _complete_purchase_and_make_change_for_selections { my($self,@bought_items) = @_; my $coin_box = $self->coin_box(); my $purchased_value = 0; foreach my $item ( @bought_items ) { $purchased_value += $item->cost_cents; } my $change_value = $coin_box->content_value() - $purchased_value; if ($change_value < 0) { die "You did not enter enough money\n"; } # Put all the user's coins into the bank my $bank = $self->bank; $coin_box->transfer_items_to_machine_location($bank); if ($change_value == 0) { return; } # List of coin types in decreasing value my @available_coin_types = map { $_->name } sort { $b->value_cents <=> $a->value_cents } Vending::CoinType->get(); my $change_dispenser = $self->change_dispenser; my @change; # Make change for the user MAKING_CHANGE: foreach my $coin_name ( @available_coin_types ) { my $coin_iter = $change_dispenser->coin_iterator(name => $coin_name); unless ($coin_iter) { die "Can't create iterator for Vending::Coin::Change\n"; } THIS_coin_type: while ( my $coin = $coin_iter->next() ) { last if $change_value < $coin->value_cents; my($change_coin) = $coin->dispense; $change_value -= $change_coin->value; push @change, $change_coin; } } if ($change_value) { #$DB::single=1; die "Not enough change\n"; } return @change; } sub _initialize_for_tests { my $self = shift; $_->delete foreach $self->inventory_items(); $_->delete foreach $self->products(); $_->delete foreach $self->items(); } 1; MachineLocation.pm100664023532023421 460612544604517 17074 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::MachineLocation; use strict; use warnings; use Vending; class Vending::MachineLocation { table_name => 'MACHINE_LOCATION', id_by => [ machine_location_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, label => { is => 'varchar', is_optional => 1 }, is_buyable => { is => 'integer' }, cost_cents => { is => 'integer', is_optional => 1 }, items => { is => 'Vending::Content', reverse_as => 'machine_location', is_many => 1 }, coins => { is => 'Vending::Coin', reverse_as => 'machine_location', is_many => 1 }, count => { calculate => q(my @obj = $self->items; return scalar(@obj);), doc => 'How many items are in this machine_location' }, content_value => { calculate => q(my @obj = $self->items; my $val = 0; $val += $_->isa('Vending::Coin') ? $_->value_cents : $_->cost_cents foreach @obj; return $val;), doc => 'Value of all the items in this machine_location' }, content_value_dollars => { calculate_from => 'content_value', calculate => q(sprintf("\$%.2f", $content_value/100)), doc => 'Value of all the contents in dollars' }, price => { calculate_from => 'cost_cents', calculate => q(sprintf("\$%.2f", $cost_cents/100)), doc => 'display price in dollars' }, machine => { is => 'Vending::Machine', id_by => 'machine_id', constraint_name => 'MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK' }, machine_id => { is => 'integer' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'represents a "machine_location" in the machine, such as "A", "B", "user","change"', }; sub transfer_items_to_machine_location { my($self,$to_machine_location) =@_; my $to_machine_location_id = $to_machine_location->id; my @objects = $self->items(); $_->machine_location_id($to_machine_location_id) foreach @objects; return scalar(@objects); } 1; Merchandise.pm100664023532023421 171312544604517 16255 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Merchandise; use strict; use warnings; use Vending; class Vending::Merchandise { is => [ 'Vending::Content' ], table_name => 'merchandise', id_sequence_generator_name => 'URMETA_coin_coin_ID_seq', id_by => [ merchandise_id => { is => 'integer' }, ], has => [ product => { is => 'Vending::Product', id_by => 'product_id', constraint_name => 'inventory_product_ID_product_product_ID_FK' }, insert_date => { is => 'datetime' }, product_id => { is => 'integer', implied_by => 'product' }, name => { via => 'product' }, cost_cents => { via => 'product' }, price => { via => 'product' }, manufacturer => { via => 'product' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'instances of things the machine will sell and dispense', }; sub subtype_name_resolver { return __PACKAGE__; } 1; Product.pm100664023532023421 160712544604517 15455 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Product; use strict; use warnings; use Vending; class Vending::Product { is => [ 'Vending::ContentType' ], table_name => 'PRODUCT', id_sequence_generator_name => 'URMETA_content_type_TYPE_ID_seq', id_by => [ product_id => { is => 'integer' }, ], has => [ manufacturer => { is => 'varchar' }, cost_cents => { is => 'integer' }, price => { calculate_from => 'cost_cents', calculate => q(sprintf("\$%.2f", $cost_cents/100)), doc => 'display price in dollars' }, item_type_product => { is => 'Vending::ContentType', id_by => 'product_id', constraint_name => 'PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'kinds of things the machine sells', }; 1; ReturnedItem.pm100664023532023421 333412544604517 16443 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::ReturnedItem; use strict; use warnings; use Vending; class Vending::ReturnedItem { has => [ name => { is => 'String' }, value => { is => 'Float' }, source_machine_location => { is => 'Vending::MachineLocation', id_by => 'source_machine_location_id' }, price => { via => 'source_machine_location', to => 'price' }, cost_cents => { via => 'source_machine_location', to => 'cost_cents' }, ], doc => 'Represents a thing being returned to the user, not stored in the database', }; # Create Vending::ReturnedItem objects from a product or coin # To enforce vending machine rules, the passed-in item is deleted sub create_from_vend_items { my($class,@items) = @_; my $transaction = UR::Context::Transaction->begin(); my @returned_items = eval { my @returned_items; foreach my $item ( @items ) { my %create_params = ( name => $item->name, source_machine_location_id => $item->machine_location_id ); if ($item->isa('Vending::Coin')) { $create_params{'value'} = $item->value_cents; } elsif ($item->isa('Vending::Merchandise')) { $create_params{'value'} = $item->cost_cents; } else { die "Can't create a Vending::ReturnedItem from an object of type ".$item->class; } $item->delete(); my $returned_item = $class->create(%create_params); push @returned_items, $returned_item; } return @returned_items; }; if ($@) { $class->error_message($@); $transaction->rollback(); } else { $transaction->commit(); return @returned_items; } } 1; Vocabulary.pm100664023532023421 20612544604517 16116 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingpackage Vending::Vocabulary; use warnings; use strict; use UR; class Vending::Vocabulary { is => [ 'UR::Vocabulary' ], }; 1; get_coin_by_value.pl100664023532023421 63112544604517 17465 0ustar00abrummetgsc000000000000UR-0.44/t/Vendinguse strict; use warnings; use above 'Vending'; # Requires a 3-table database join (COIN, CONTENT and CONTENT_TYPE), plus a # cross-datasource join to Vending::CoinType my @coins = Vending::Coin->get(value_cents => 25); print "Found ",scalar(@coins)," coins:\n"; foreach my $coin ( @coins ) { printf("id %s name %s value %d in slot %s\n",$coin->id, $coin->name, $coin->value_cents, $coin->slot_name); } machine_classes_1.uxf100664023532023421 2341712544604517 17607 0ustar00abrummetgsc000000000000UR-0.44/t/Vending com.umlet.element.base.Relation 130 200 260 220 lt=<<- //subject_id:Vending::VendSlot;UR::Entity;; 240;200;20;20 com.umlet.element.base.Relation 495 320 340 120 lt=<<- //subject_id:UR::Singleton;UR::Object;; 20;100;320;20 com.umlet.element.base.Class 420 180 290 160 Vending::Coin -- value_cents: name: String +coin_id: integer item_type: Vending::ItemType coin_type: Vending::CoinType type_id: integer subtype_name: //subject_id:Vending::Coin com.umlet.element.base.Relation 545 120 325 240 lt=<<- //subject_id:Vending::Coin;Vending::VendItem;; 305;20;20;220 com.umlet.element.base.Relation 260 272 510 88 lt=<- //subject_id:Vending::VendSlot;UR::Singleton;machine_id;id 490;68;20;20 com.umlet.element.base.Class 720 20 290 100 Vending::CoinType -- type_id: value_cents: integer item_type: Vending;:ItemType +name: String //subject_id:Vending::CoinType com.umlet.element.base.Class 320 380 100 20 UR::Entity //subject_id:UR::Entity com.umlet.element.base.Relation 260 326 1080 142 lt=<- //subject_id:Vending::ReturnedItem;Vending::VendSlot;source_slot_id;slot_id 20;20;1060;122 com.umlet.element.base.Class 20 220 260 280 Vending::VendSlot -- items: Vending::VendItem label: varchar content_value: machine_id: Scalar cost_cents: integer content_value_dollars: +slot_id: integer count: coins: Vending::Coin price: machine: UR::Singleton is_buyable: integer name: varchar //subject_id:Vending::VendSlot com.umlet.element.base.Relation 180 0 655 380 lt=<<- //subject_id:Vending::Machine;UR::Singleton;; 635;360;20;20 com.umlet.element.base.Class 1050 60 220 100 Vending::Product -- price: +product_id: integer manufacturer: varchar cost_cents: integer //subject_id:Vending::Product com.umlet.element.base.Class 1320 340 310 140 Vending::ReturnedItem -- cost_cents: price: value: Float source_slot: Vending::VendSlot name: String source_slot_id: integer //subject_id:Vending::ReturnedItem com.umlet.element.base.Class 720 140 260 160 Vending::VendItem -- machine_id: Scalar slot: Vending::VendSlot machine: Vending::Machine subtype_name: varchar +vend_item_id: integer slot_id: integer slot_name: //subject_id:Vending::VendItem com.umlet.element.base.Class 750 340 130 20 UR::Singleton //subject_id:UR::Singleton com.umlet.element.base.Relation 830 120 340 280 lt=<<- //subject_id:Vending::Merchandise;Vending::VendItem;; 20;20;320;260 com.umlet.element.base.Class 420 20 260 120 Vending::ItemType -- machine_id: Scalar machine: Vending::Machine name: varchar count: +type_id: integer //subject_id:Vending::ItemType com.umlet.element.base.Relation 690 72 50 164 lt=<- //subject_id:Vending::Coin;Vending::CoinType;name;name 30;20;20;144 com.umlet.element.base.Relation 350 360 185 80 lt=<<- //subject_id:UR::Entity;UR::Object;; 165;60;20;20 com.umlet.element.base.Relation 350 0 220 420 lt=<<- //subject_id:Vending::ItemType;UR::Entity;; 20;400;200;20 com.umlet.element.base.Relation 260 228 480 138 lt=<- //subject_id:Vending::VendItem;Vending::VendSlot;slot_id;slot_id 20;118;460;20 com.umlet.element.base.Relation 360 0 80 58 lt=<- //subject_id:Vending::ItemType;Vending::Machine;machine_id;id 20;20;60;38 com.umlet.element.base.Class 20 20 360 180 Vending::Machine -- items: Vending::VendItem coin_box_slot: products: Vending::Product bank_slot: change_dispenser: slots: Vending::VendSlot inventory_items: Vending::Merchandise item_types: Vending::ItemType //subject_id:Vending::Machine com.umlet.element.base.Relation 495 320 1000 120 lt=<<- //subject_id:Vending::ReturnedItem;UR::Object;; 20;100;980;20 com.umlet.element.base.Relation 1030 76 270 286 lt=<- //subject_id:Vending::Merchandise;Vending::Product;product_id;product_id 20;20;250;266 com.umlet.element.base.Relation 400 90 40 218 lt=<- //subject_id:Vending::Coin;Vending::ItemType;type_id;type_id 20;20;20;198 com.umlet.element.base.Relation 350 0 535 420 lt=<<- //subject_id:Vending::CoinType;UR::Entity;; 20;400;515;20 com.umlet.element.base.Relation 530 0 650 180 lt=<<- //subject_id:Vending::Product;Vending::ItemType;; 20;20;630;160 com.umlet.element.base.Class 1020 180 260 200 Vending::Merchandise -- manufacturer: name: product: Vending::Product +inv_id: integer cost_cents: insert_date: datetime price: subtype_name: product_id: integer //subject_id:Vending::Merchandise com.umlet.element.base.Relation 360 0 380 178 lt=<- //subject_id:Vending::VendItem;Vending::Machine;machine_id;id 20;20;360;158 com.umlet.element.base.Class 460 380 110 40 UR::Object -- +id: Scalar //subject_id:UR::Object com.umlet.element.base.Relation 350 120 520 300 lt=<<- //subject_id:Vending::VendItem;UR::Entity;; 20;280;500;20 notes.txt100664023532023421 355012544604517 15367 0ustar00abrummetgsc000000000000UR-0.44/t/Vendingur define namespace Vending ur define datasource sqlite --dsname Machine Schema: -- name the kinds of things the machine knows about create table item_type (type_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL); -- places where things get sloted in create table vend_slot (slot_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, is_buyable integer NOT NULL, cost_cents integer, label varchar); -- kinds of coins we'll accept and their value --create table coin_type(type_id integer PRIMARY KEY NOT NULL REFERENCES item_type(type_id), -- value_cents integer NOT NULL); --Parent table for instances of things the machine can sell create table vend_item (vend_item_id integer PRIMARY KEY NOT NULL, subtype_name varchar, slot_id integer NOT NULL REFERENCES vend_slot(slot_id)); -- instances of coins held by the machine create table coin (coin_id integer PRIMARY KEY NOT NULL REFERENCES vend_item(vend_item_id), type_id integer NOT NULL REFERENCES item_type(type_id)); -- kinds of things we'll sell create table product (product_id integer PRIMARY KEY NOT NULL REFERENCES item_type(type_id), cost_cents integer NOT NULL, manufacturer varchar NOT NULL); -- instances of things in the inventory create table inventory (inv_id integer PRIMARY KEY NOT NULL, product_id integer NOT NULL REFERENCES product(product_id), insert_date datetime NOT NULL DEFAULT (date('now'))); ur update classes fixup VendingMachine::Inventory add indirect properties for name, price, is_sellable make command line script vend Make skeleton VendingMachine::Command buy_a_different_change.t100664023532023421 307412544604517 20561 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 18; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $dime_type = Vending::CoinType->get(name => 'dime'); my $nickel_type = Vending::CoinType->get(name => 'nickel'); # 5 dimes and 5 nickels my $change_disp = $machine->change_dispenser; foreach ( 1 .. 5 ) { ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $nickel_type), 'Added a nickel to the change'); ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $dime_type), 'Added a dime to the change'); } my $prod = Vending::Product->create(name => 'Orange', manufacturer => 'Acme', cost_cents => 65); ok($prod, 'Defined "Orange" product'); my $slot_a = $machine->machine_locations(name => 'a'); my $inv = $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($inv, 'Added an orange to slot A'); ok($machine->insert('dollar'), 'Inserted a dollar'); my @items = $machine->buy('a'); is(scalar(@items), 5, 'Got back five items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'Orange'}, 1, 'One of them was an Orange'); is($item_counts{'nickel'}, 1, 'One of them was a nickel'); is($item_counts{'dime'}, 3, 'Three of them were dimes'); buy_a_get_change_back.t100664023532023421 273212544604517 20352 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $quarter_type = Vending::CoinType->get(name => 'quarter'); my $dime_type = Vending::CoinType->get(name => 'dime'); my $change_disp = $machine->change_dispenser; ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $quarter_type), "Added a quarter to the change"); ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $dime_type), "Added a dime to the change"); my $prod = Vending::Product->create(name => 'Battery', manufacturer => 'Acme', cost_cents => 65); ok($prod, "defined Battery product"); my $slot_a = Vending::MachineLocation->get(name => 'a'); $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($machine->insert('dollar'), 'Inserted a dollar'); my @items = $machine->buy('a'); is(scalar(@items), 3, 'Got back three items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'Battery'}, 1, 'One of them was a Battery'); is($item_counts{'quarter'}, 1, 'One of them was a quarter'); is($item_counts{'dime'}, 1, 'One of them was a dime'); buy_a_not_enough_change.t100664023532023421 401512544604517 20754 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 16; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine, but not enough change my $quarter_type = Vending::CoinType->get(name => 'quarter'); my $change_disp = $machine->change_dispenser; ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $quarter_type),'Added a quarter to the change'); my $prod = Vending::Product->create(name => 'Orange', manufacturer => 'Acme', cost_cents => 65); ok($prod, "Defined 'Orange' product"); my $slot_a = Vending::MachineLocation->get(name => 'a'); my $inv = $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($inv, 'Added an orange to slot A'); ok($machine->insert('dollar'), 'Inserted a dollar'); my @errors; $machine->dump_error_messages(0); $machine->error_messages_callback(sub { push @errors, $_[1]; }); my @items = $machine->buy('a'); is(scalar(@items), 0, 'Got no items'); like($errors[0], qr(Not enough change), 'Error message indicated not enough change'); @items = $machine->coin_return(); is(scalar(@items),1, 'Coin return got us one thing back'); is($items[0]->name, 'dollar', 'The returned thing was a dollar'); is($items[0]->value, 100, 'The returned thing was worth 100 cents'); # Poke the machine and make sure everything is still in there @items = Vending::Merchandise->get(); is(scalar(@items), 1, 'There is one item still in the inventory'); is($items[0]->name, 'Orange', 'It was an Orange'); is($items[0]->machine_location, $slot_a, 'The orange is in slot a'); my $bank = $machine->bank(); @items = $bank->items(); is(scalar(@items), 0, 'Nothing in the bank'); @items = $change_disp->items(); is(scalar(@items), 1, 'One thing in the change dispenser'); is($items[0]->name, 'quarter', 'It is a quarter'); buy_b_not_enough_money.t100664023532023421 301512544604517 20656 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $prod = Vending::Product->create(name => 'Candy', manufacturer => 'Acme', cost_cents => 100); ok($prod, 'Defined Candy product'); my $slot_b = Vending::MachineLocation->get(name => 'b'); ok($slot_b->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod),'Added Candy to slot a'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('nickel'), 'Inserted a nickel'); my @errors; $machine->dump_error_messages(0); $machine->error_messages_callback(sub { push @errors, $_[1]; }); my @items = $machine->buy('b'); is(scalar(@items), 0, 'Got back no items'); like($errors[0], qr/You did not enter enough money/, 'Error message indicates we did not enter enough money'); @items = $machine->coin_return(); is(scalar(@items), 4, 'Coin return got back 4 items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'quarter'}, 3, 'Three of them were quarters'); is($item_counts{'nickel'}, 1, 'One of them was a nickel'); buy_b_with_exact_change.t100664023532023421 220312544604517 20744 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 10; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $prod = Vending::Product->create(name => 'Apple', manufacturer => 'Acme', cost_cents => 100); ok($prod, 'Created a product type Apple'); my $slot = Vending::MachineLocation->get(name => 'b'); ok($slot, 'Got object for slot b'); my $item = $slot->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($item, 'Added an Apple inventory item to slot b'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); my @items = $machine->buy('b'); is(scalar(@items), 1, 'Got back one item'); is($items[0]->name, 'Apple', 'It was an Apple'); coin_return.t100664023532023421 125412544604517 16454 0ustar00abrummetgsc000000000000UR-0.44/t/Vending/tuse strict; use warnings; use Test::More tests => 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); my @items = $machine->coin_return(); is(scalar(@items), 2, 'Got back two items'); is($items[0]->name, 'quarter', 'Item 1 is a quarter'); is($items[1]->name, 'quarter', 'Item 2 is a quarter'); vend100775023532023421 21312544604517 14331 0ustar00abrummetgsc000000000000UR-0.44/t/Vending#!/gsc/bin/perl use strict; use warnings; use above 'UR'; use above "Vending"; Vending::Command->execute_with_shell_params_and_exit(); vend_interactive.pl100775023532023421 332712544604517 17371 0ustar00abrummetgsc000000000000UR-0.44/t/Vending#!/gsc/bin/perl use strict; use warnings; use above 'Vending'; my $machine = Vending::Machine->get(); unless ($machine) { print STDERR "Out of order...\n"; exit; } my %command_map = ( help => \&help, done => \&done, 'check-again' => \&clear_query_cache, 'coin-return' => 'Vending::Command::CoinReturn', dollar => 'Vending::Command::Dollar', quarter => 'Vending::Command::Quarter', dime => 'Vending::Command::Dime', nickel => 'Vending::Command::Nickel', buy => 'Vending::Command::Buy', menu => 'Vending::Command::Menu', ); $|=1; &help(); while (1) { print "command> "; my $line = <>; last unless $line; chomp($line); my @words = split(/\s+/, $line); my $thing = $command_map{shift @words}; if (ref($thing)) { # It's a sub we can just call $thing->(); } elsif($thing) { # It's a command class name my $command = $thing->create(bare_args => \@words); if ($command->execute() ) { UR::Context->commit(); } } else { print "That is not a valid command\n"; } } &done(); sub done { print "\nGoodbye\n"; exit(0); } sub help { print q( Vendco Vending Machine available commands: dollar - insert a dollar quarter - insert a quarter dime - insert a dime nickel - insert a nickel menu - See what is available buy - purchase an item from the menu coin-return - return any coins you inserted help - this help text check-again - secret backdoor to use when another progrtam reloads the inventory ); } sub clear_query_cache { print "Forgetting about Vending::Merchandises and Vending::Coins\n"; Vending::Merchandise->unload(); Vending::Coin::Change->unload(); } above.t100664023532023421 242412544604517 13364 0ustar00abrummetgsc000000000000UR-0.44/t#!/usr/bin/env perl use strict; use warnings; use File::Temp; use Test::More tests => 4; use IO::File; BEGIN { $ENV{'PERL_ABOVE_QUIET'} = 1 if $ENV{'HARNESS_ACTIVE'}; } my $d = File::Temp::tempdir(CLEANUP => 1); ok($d, "created working directory $d"); mkdir "$d/lib1" or die $!; mkdir "$d/lib2" or die $!; IO::File->new(">$d/lib1/Foo.pm")->print('package Foo; 1'); IO::File->new(">$d/lib2/Foo.pm")->print('package Foo; 1'); mkdir "$d/lib1/Foo"; mkdir "$d/lib2/Foo"; eval 'use lib "$d/lib1/"'; chdir "$d/lib2/Foo"; eval "use above 'Foo'"; is(&clean_darwin($INC{"Foo.pm"}), "$d/lib2/Foo.pm", "used the expected module"); chdir "$d/" or die "Failed to chdir to $d/: $!"; my $src = $^X . q| -e 'use above "Foo"; print $INC{"Foo.pm"}'|; my $v = `$src`; is(&clean_darwin($v), "$d/lib2/Foo.pm", "Got the original module, not the 2nd one, and not an error."); chdir "$d/lib1" or die "Failed to chdir to $d/lib1: $!"; $v = `$src`; is(&clean_darwin($v), "$d/lib2/Foo.pm", "Got the original module, not the 2nd one, and not an error."); chdir "$d/.."; # So File::Temp can remove $d exit(0); # remove the /private from Mac OS X paths sub clean_darwin { my ($path) = @_; $path =~ s#//#/#g; return $path unless $^O eq 'darwin'; $path =~ s{^/private}{}; return $path; } Person.pm100664023532023421 60412544604517 25224 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/classes/URTAlternatepackage URTAlternate::Person; use URTAlternate; use strict; use warnings; class URTAlternate::Person { table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, ], schema_name => 'TheDB', data_source => 'URTAlternate::DataSource::TheDB', }; sub uc_name { return uc(shift->name); } 1; Vocabulary.pm100664023532023421 51712544604517 26070 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/classes/URTAlternatepackage URTAlternate::Vocabulary; use strict; use warnings; use UR::Object::Type; use URTAlternate; class URTAlternate::Vocabulary { is => ['UR::Vocabulary'], doc => 'A set of words for a given namespace.', }; my @words_with_special_case = (qw//); sub _words_with_special_case { return @words_with_special_case; } 1; Meta.pm100664023532023421 114012544604517 27546 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/data_source/URTAlternate/DataSourcepackage URTAlternate::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use UR; UR::Object::Type->define( class_name => 'URTAlternate::DataSource::Meta', is => ['UR::DataSource::Meta'], ); use File::Temp; # Override server() so we can make the metaDB file in # a temp dir sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . "_ur_testsuite_metadb" . $self->_extension_for_db; return $PATH; } END { our $PATH; unlink $PATH if ($PATH); } 1; Meta.sqlite3-dump100664023532023421 645012544604517 31472 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/data_source/URTAlternate/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('URTAlternate::DataSource::TheDB','main','car','car_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('URTAlternate::DataSource::TheDB','main','person','person_id',1); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('URTAlternate::DataSource::TheDB','main','car','TABLE','entity',NULL,'2011-04-20 17:07:18',NULL); INSERT INTO "dd_table" VALUES('URTAlternate::DataSource::TheDB','main','person','TABLE','entity',NULL,'2011-04-20 17:07:18',NULL); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','model','varchar',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','person','name','varchar',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','car_id','integer',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','person','person_id','integer',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','make','varchar',NULL,'N','2011-04-20 17:07:18',''); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; TheDB.pm100664023532023421 61312544604517 27572 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/data_source/URTAlternate/DataSourcepackage URTAlternate::DataSource::TheDB; use strict; use warnings; use File::Temp; use URTAlternate; class URTAlternate::DataSource::TheDB { is => ['UR::DataSource::SQLite'], }; sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . '_ur_testsuite_db' . $self->_extension_for_db; return $PATH; } END { our $PATH; unlink $PATH if $PATH; } 1; TheDB.sqlite3-dump100664023532023421 27412544604517 31510 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/data_source/URTAlternate/DataSourceCREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, name varchar NOT NULL); CREATE TABLE car (car_id integer NOT NULL PRIMARY KEY, make varchar NOT NULL, model varchar NOT NULL); Car.pm100664023532023421 56212544604517 25510 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/more_classes/URTAlternatepackage URTAlternate::Car; use URTAlternate; use strict; use warnings; class URTAlternate::Car { table_name => 'car', id_by => [ car_id => { is => 'integer' }, ], has => [ make => { is => 'varchar' }, model => { is => 'varchar' }, ], schema_name => 'TheDB', data_source => 'URTAlternate::DataSource::TheDB', }; 1; URTAlternate.pm100664023532023421 27012544604517 24254 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/namespaceuse strict; use warnings; package URTAlternate; use UR; class URTAlternate { is => ['UR::Namespace'], doc => 'A dummy namespace used by the UR test suite.', }; 1; #$Header 01_namespace.t100664023532023421 341312544604517 22356 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../namespace'; use lib File::Basename::dirname(__FILE__).'/../classes'; use lib File::Basename::dirname(__FILE__).'/../data_source'; use lib File::Basename::dirname(__FILE__).'/../more_classes'; use URTAlternate; is(URTAlternate->class, 'URTAlternate', 'Namespace name'); my $class_meta = URTAlternate->get_member_class('URTAlternate::Person'); ok($class_meta, 'get_member_class'); is($class_meta->class_name, 'URTAlternate::Person', 'get_member_class returned the right class'); # This is basically the list of Perl modules under URT/ # note that the 38* classes do not compile because they use data sources that exist # only during that test, and so are not returned by get_material_classes() my @expected_class_names = sort map { 'URTAlternate::' . $_ } qw( Person Car DataSource::Meta DataSource::TheDB Vocabulary ); my @class_metas = sort URTAlternate->get_material_classes; is(scalar(@class_metas), scalar(@expected_class_names), 'get_material_classes returned expected number of items'); foreach (@class_metas) { isa_ok($_, 'UR::Object::Type'); } my @class_names = sort map { $_->class_name } @class_metas; is_deeply(\@class_names, \@expected_class_names, 'get_material_classes'); my @data_sources = sort URTAlternate->get_data_sources; foreach ( @data_sources) { isa_ok($_, 'UR::DataSource'); } my @expected_ds_names = map { 'URTAlternate::' . $_ } qw( DataSource::Meta DataSource::TheDB ); my @data_source_names = sort map { $_->class } @data_sources; is_deeply(\@data_source_names, \@expected_ds_names, 'get_data_sources'); 02_update_classes.t100664023532023421 1165312544604517 23447 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/tuse strict; use warnings; use Test::More; use File::Basename; use File::Temp; use Cwd; use lib Cwd::abs_path(File::Basename::dirname(__FILE__)."/../../../lib"); # for UR our $initial_dir; our $tempdir; BEGIN { eval "use Archive::Tar"; if (1) { plan skip_all => 'this always fails during cpanm install for an unknown reason', } elsif ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'skip running during install', exit; } elsif ($@ =~ qr(Can't locate Archive/Tar.pm in \@INC)) { plan skip_all => 'Archive::Tar does not exist on the system'; exit; } else { plan tests => 36; } $initial_dir = Cwd::cwd; my $tarfile = Cwd::abs_path(File::Basename::dirname(__FILE__).'/02_update_classes.tar.gz'); $tempdir = File::Temp::tempdir(CLEANUP => 1); chdir($tempdir); my $tar = Archive::Tar->new($tarfile); ok($tar->extract, 'Extract initial filesystem'); } END { chdir $initial_dir; # so File::Temp can clean up the tempdir } use lib $tempdir.'/namespace'; use lib $tempdir.'/classes'; use lib $tempdir.'/data_source'; use lib $tempdir.'/more_classes'; use URTAlternate; UR::DBI->no_commit(0); # UR's test harness defaults to no_commit = 1 my $cmd = UR::Namespace::Command::Update::ClassesFromDb->create(namespace_name => 'URTAlternate'); ok($cmd, 'Create update classes command'); $cmd->queue_status_messages(1); $cmd->queue_warning_messages(1); $cmd->queue_error_messages(1); $cmd->dump_status_messages(0); $cmd->dump_warning_messages(0); $cmd->dump_error_messages(0); ok($cmd->execute, 'execute update classes with no changes'); my $messages = join("\n",$cmd->status_messages()); like($messages, qr(Updating namespace: URTAlternate), 'Status message showing namespace'); like($messages, qr(Found data sources: TheDB), 'Found the data source'); like($messages, qr(No data schema changes), 'No schema changes'); like($messages, qr(No class changes), 'No class changes'); my @messages = $cmd->warning_messages(); is(scalar(@messages), 0, 'no warning messages'); @messages = $cmd->error_messages(); is(scalar(@messages), 0, 'no error messages'); my $dbh = URTAlternate::DataSource::TheDB->get_default_handle(); ok($dbh, 'Got handle for database'); ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY REFERENCES person(person_id), office varchar NOT NULL)'), 'Add employee table'); ok($dbh->do('ALTER TABLE car ADD COLUMN owner_id integer REFERENCES person(person_id)'), 'Add owner_id column to car table'); ok($dbh->commit, 'commit table changes'); # SQLite seems to have an issue where "PRAGMA foreign_key_list()" doesn't return # the newly added foreign key info from the ALTER TABLE car. Workaround is to disconnect # and re-connect URTAlternate::DataSource::TheDB->disconnect_default_handle(); $dbh = URTAlternate::DataSource::TheDB->get_default_handle(); my $sth = $dbh->prepare("PRAGMA foreign_key_list(car)"); $sth->execute(); my $data = $sth->fetchall_arrayref(); $cmd = UR::Namespace::Command::Update::ClassesFromDb->create(namespace_name => 'URTAlternate', _override_no_commit_for_filesystem_items => 1); ok($cmd, 'Create update classes command after adding table'); $cmd->dump_status_messages(1); $cmd->dump_warning_messages(1); $cmd->dump_error_messages(1); $cmd->dump_status_messages(0); $cmd->dump_warning_messages(0); $cmd->dump_error_messages(0); ok($cmd->execute(), 'execute update classes after changes'); ok(-f "${tempdir}/namespace/URTAlternate.pm", 'Namespace module exists'); ok(-f "${tempdir}/data_source/URTAlternate/DataSource/TheDB.pm", 'Data source module exists'); ok(-f "${tempdir}/classes/URTAlternate/Person.pm", 'Person module exists'); ok(-f "${tempdir}/more_classes/URTAlternate/Car.pm", 'Car module exists'); ok(-f "${tempdir}/namespace/URTAlternate/Employee.pm", 'Employee module exists'); # new stuff gets created in the namespace dir foreach my $class_props ( [ 'URTAlternate::Person' => ['person_id', 'name'] ], [ 'URTAlternate::Car' => ['car_id', 'make', 'model', 'owner_id', 'person_owner'] ], [ 'URTAlternate::Employee' => ['employee_id', 'office', 'person_employee'] ] ) { my($class_name, $expected_properties) = @$class_props; my $class_meta = $class_name->__meta__; ok($class_meta, "Got class metaobject for $class_name"); my %expected_properties = map { $_ => 1 } @$expected_properties; my %got_properties = map { $_->property_name => 1 } UR::Object::Property->get(class_name => $class_name); foreach my $property ( keys %expected_properties ) { ok(delete $got_properties{$property}, "Found property $property for class $class_name"); } ok(!scalar(keys %got_properties), 'no extra properties'); if (keys %got_properties) { diag('Extra properties that were not expected: ', join(', ', keys %got_properties)); } } 02_update_classes.tar.gz100664023532023421 375512544604517 24375 0ustar00abrummetgsc000000000000UR-0.44/t/alternate_namespace_layout/t‹ ÖOí[ûsÚ¸ÎÏüš6³&3¤±ÍëL3Kî6³)éÒ™}x„­mü ’Ü4·›û·ß#ÙLÃ#Ûõù!ëut>éè;Ùv1焟lQtz½ Ÿf¹Z6ÕgÅ4ÔóX £Z-×̲i”tèÕT=Ø„\`†Ð°ÐóˆXToÈíƒïPìÿ«n¿å Â|,ÈÉ^𯕚©ð¯P=Ç_ø l<]Ìî_¼Íà_«Uâ_5ê3øCmÀ_ÏñߺŒ°}‹‡%ño4&  Y(„œ .µES}¿Ã̧þÇEWÝFãrð'±E£Ñ¿‘ñãI‡Í‚Zf A_ „rôúýªÉ'…Úï%Uê¶,ÖZˆ‚kt0‡£ë€!Œ†ô3ñ‘=ÂaFä•V*<€"Þ=úQU³î¨¸±øˆØ»–AÁרøéîääªñp€¬Eõ"Ý!óöÖ,< F³ð}ìÿ„ñÀßÈÞϲÿkz%>ÿ«f¹lÈý¯f¾ÿ÷·ÿ£º“—¸ƒ´Mõo".±ä.U;y¤Ê´h{SÇÜ«ý¯þ••[Ô‘¿ÆîA£¾ CÂ4ô5ŒÝà æÓ­‡7üŒ™}ƒçrû†xx¢Uÿ†œ½‰•r°ÀBfGeÓs;ƒÒž*Ï·zˆÝIhG=NyÐ.òz-ŽOeÙÑsp‰n-Xƒÿ›u³ûß„ï9ÿÛ5þÛŠÖ¿ õŒª^©çøïÿ‰s;Ù þu°7ë¹ÿ놞ÇÏÿ÷Dà§ÁüÏÔáÙ^=Ñsþ·?þ—d6} 5/PEˆb>$/°–C¸Íèx PM=^Bvà†žÏöYŸÐ¡Ý’{ލ×dCç&ýfˆ7áËlÐy|êkê“¢¢[ŠˆNÝò¹=òÏDð9W¨âQ<ü¿©+éñFÊ(—Ÿ cÔ ƒ¯Å#ÄtG}äá[¢&)Ítö]CS˜7´ÂH@È¡,b‹Qã˜,BÐzȉ{ ªb‹0Ž| ú Ã­þ»¦úW}Eýõ:¡Q£!¼L´x…^X!³ႇTKA5xªóãS‹|Äç6@c9ƒf’ªÆ#=È9žIhÄ€yƒb Âù¯`GŽÔda¢ô±Ð—uÆ0ƒ@Ñ™O8‡{Ô£ÛÃúË‘/””5¨iã÷ôþsúG1ºøÂ"õ©€à›þ¹q‹˜ X<CþhÅVÕŠò!zý?äüÑ%Ç6#X@/'Gqse]ëWãwhú°®"Ã3‰ì»'u}¨âo^ ø$¾ â^+)m5S7Œc½rlêȨ7ôzÃøWT´ÁñÇëd}R—Òóð jÜ%ëI•»ÄŠ›é±ýÐuÕŽØÀé—ºÎÖqÒ³9ó§ú"‚d>¿Šïh À×´Ík2^„rª{ÖeÚCÜ«Y>|¯ Å+ß® Ò¬¯}ú)$;çYhàjï²µ˜æXsëòýûóþ÷› Éÿ«5ö„VÄÿFU¯Mÿþcêåz9ÿŸGþ_¡ß\••ŸJ‹g»ÿ57HÊ5°©d°JË\üã%‹m¤Íµé´¹3Ðò”ù?'ež=g>Aûo+Ïîÿ¿=¼êþ8}uÿ£¢W+5³&ó¿e=¿ÿ·™¢}‹EÅÉÅ»ÙÌM’Y•P*›å’@IQ1bë+ºS¿—Î:¤âŸ´aò_ož.^À€äo÷% ì÷¿ª•²ô†Y7óû»ÇK³â?öÿF*äøïÿ·˜mè €•ïÿÔÌükùù¿×ø°ßÄåè&ýæ¿JS-ºös…o¹ó¯ÄÒKÿª–"{y5àÙÅ ãW¦¶øðZïÖMyþµüþ÷ŽñßÞÀkÿ†ôÿ•ª¡çøïÿ½ú—1þŸÝÿU½œßÿ:ØÍô¦uÓ¨Aòöü‰Ÿ’Àí<®¯ù×x°÷ýä];È€Ìó]u£TŸJ¾šž/ßì–Çþ¹ä’K.¹ä’K.¹ä’K.kÊÿu»‚ÙPinternal.t100664023532023421 1577612544604517 17012 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../lib"; use lib File::Basename::dirname(__FILE__)."/.."; use lib File::Basename::dirname(__FILE__)."/test_namespace/"; use UR; use strict; use warnings; use Cwd; plan tests => 38; our $NAMESPACE = 'Testing'; my $base_dir = Cwd::abs_path(File::Basename::dirname(__FILE__)); our %expected_class_data = ( Testing => { name => 'Testing', is => ['UR::Namespace'], relpath => 'Testing.pm', file => 'Testing.pm', }, 'Testing::Something' => { name => 'Testing::Something', is => ['UR::Object'], relpath => 'Testing/Something.pm', file => 'Something.pm', }, 'Testing::Something::SubClass1' => { name => 'Testing::Something::SubClass1', is => ['Testing::Something'], relpath => 'Testing/Something/SubClass1.pm', file => 'SubClass1.pm', }, 'Testing::Something::SubClass2' => { name => 'Testing::Something::SubClass2', is => ['Testing::Something'], relpath => 'Testing/Something/SubClass2.pm', file => 'SubClass2.pm', }, 'Testing::Color' => { name => 'Testing::Color', is => ['UR::Object'], relpath => 'Testing/Color.pm', file => 'Color.pm', }, ); my $cmd = UR::Namespace::Command::Sys::ClassBrowser->create( namespace_name => $NAMESPACE, ); ok($cmd, 'Created ClassBrowser command'); test_name_cache($cmd); test_directory_tree_cache($cmd); test_name_tree_cache($cmd); test_inheritance_tree_cache($cmd); sub test_name_cache { my $cmd = shift; my $by_class_name = $cmd->_generate_class_name_cache('Testing'); strip_ids(values %$by_class_name); # IDs are random UUIDs is_deeply($by_class_name, \%expected_class_data, '_generate__class_name_cache'); } sub test_directory_tree_cache { my $cmd = shift; ok( $cmd->_load_class_info_from_modules_on_filesystem($NAMESPACE) ,'_load_class_info_from_modules_on_filesystem'); my $tree = $cmd->directory_tree_cache($NAMESPACE); ok($tree, 'Get directory tree cache'); ok($tree->has_children, 'Tree root has children'); is($tree->name, $NAMESPACE, 'Tree root name'); my $data = strip_ids($tree->data); is_deeply($tree->data, $expected_class_data{$NAMESPACE}, 'Tree root data'); my $children = $tree->children(); is(scalar(@$children), 3, 'Root has 3 children'); foreach ( { class => 'Testing::Color', file => 'Color.pm'}, { class => 'Testing::Something', file => 'Something.pm'} ) { is_deeply($tree->get_child($_->{file})->data, $expected_class_data{$_->{class}}, "get_child $_->{file}"); ok(! $tree->get_child($_->{file})->has_children, "$_->{file} has no children"); } ok(! $tree->get_child('not there'), 'Getting non-existent child returns nothing'); $tree = $tree->get_child('Something'); ok($tree, 'Get child directory "Something"'); ok($tree->has_children, 'directory "Something" has children'); foreach ( { class => 'Testing::Something::SubClass1', file => 'SubClass1.pm' }, { class => 'Testing::Something::SubClass2', file => 'SubClass2.pm' } ) { is_deeply($tree->get_child($_->{file})->data, $expected_class_data{$_->{class}}, "get_child $_->{file}"); ok(! $tree->get_child($_->{file})->has_children, "$_->{file} has no children"); } } sub test_name_tree_cache { my $cmd = shift; ok( $cmd->_load_class_info_from_modules_on_filesystem($NAMESPACE) ,'_load_class_info_from_modules_on_filesystem'); my $tree = $cmd->name_tree_cache($NAMESPACE); ok($tree, 'Get name tree cache'); ok($tree->has_children, 'Tree root has children'); is($tree->name, $NAMESPACE, 'Tree root name'); my $data = strip_ids($tree->data); is_deeply($tree->data, $expected_class_data{$NAMESPACE}, 'Tree root data'); my $children = $tree->children(); is(scalar(@$children), 2, 'Root has 2 children'); is_deeply($tree->get_child('Color')->data, $expected_class_data{'Testing::Color'}, 'get child Color'); ok(! $tree->get_child('Color')->has_children, 'Color has no children'); $tree = $tree->get_child('Something'); is_deeply($tree->data, $expected_class_data{'Testing::Something'}, 'Get child "Something"'); $children = $tree->children; is(scalar(@$children), 2, '"Something" has 2 children'); foreach my $name ( qw( SubClass1 SubClass2 )) { my $class = 'Testing::Something::'.$name; is_deeply($tree->get_child($name)->data, $expected_class_data{$class}, "Get child $name"); ok(! $tree->get_child($name)->has_children, "$name has no children"); } } sub test_inheritance_tree_cache { my $cmd = shift; ok( $cmd->_load_class_info_from_modules_on_filesystem($NAMESPACE) ,'_load_class_info_from_modules_on_filesystem'); my $tree = $cmd->inheritance_tree_cache($NAMESPACE); ok($tree, 'Get inheritance tree cache'); ok($tree->has_children, 'Tree root has children'); is($tree->name, 'UR::Object', 'UR::Object is the tree root'); my $visit; $visit = sub { my $tree = shift; return { name => $tree->name, children => [ map { $visit->($_) } sort { $a->name cmp $b->name } @{$tree->children} ], }; }; my $got_inheritance = $visit->($tree); my $expected_inheritance = { name => 'UR::Object', children => [ { name => 'Testing::Color', children => [], }, { name => 'Testing::Something', children => [ { name => 'Testing::Something::SubClass1', children => [], }, { name => 'Testing::Something::SubClass2', children => [], } ], }, { name => 'UR::Singleton', children => [ { name => 'UR::Namespace', children => [ { name => 'Testing', children => [], }, ], }, ], } ] }; is_deeply($got_inheritance, $expected_inheritance, 'Inheritance tree'); } sub strip_ids { delete $_->{id} foreach (@_); } Testing.pm100664023532023421 14712544604517 21701 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser/test_namespacepackage Testing; use strict; use warnings; use UR; class Testing { is => 'UR::Namespace', }; 1; Color.pm100664023532023421 25212544604517 22754 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser/test_namespace/Testingpackage Testing::Color; use strict; use warnings; use Testing; class Testing::Color { id_by => 'color_name', has => [qw( redness greenness blueness ) ], }; 1; Something.pm100664023532023421 61512544604517 23636 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser/test_namespace/Testingpackage Testing::Something; use strict; use warnings; class Testing::Something { id_by => [ prop1 => { is => 'String' }, prop2 => { is => 'Integer' }, ], has => [ color => { is => 'Testing::Color', id_by => 'color_id' }, ], is_abstract => 1, doc => 'A class with some properties', }; sub a_method { 1; } sub another_method { 2; } 1; SubClass1.pm100664023532023421 30712544604517 25434 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser/test_namespace/Testing/Somethingpackage Testing::Something::SubClass1; use strict; use warnings; class Testing::Something::SubClass1 { is => 'Testing::Something', has => [ age => { is => 'Integer' }, ], }; 1; SubClass2.pm100664023532023421 35012544604517 25433 0ustar00abrummetgsc000000000000UR-0.44/t/class_browser/test_namespace/Testing/Somethingpackage Testing::Something::SubClass2; use strict; use warnings; class Testing::Something::SubClass2 { is => 'Testing::Something', has => [ coolness => { is => 'Integer' }, ], }; sub cool_method { 'a'; } 1; 01_command_define_namespace.t100664023532023421 355712544604517 22212 0ustar00abrummetgsc000000000000UR-0.44/t/newnamespace#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../lib"; use File::Temp; use Cwd; use UR; my $current_dir = Cwd::cwd; END { chdir $current_dir }; # so the temp dir can get cleaned up my $tempdir = File::Temp::tempdir(CLEANUP => 1); ok($tempdir, 'make temp dir'); chdir($tempdir); push @INC,$tempdir; # so it can find the namespace modules it's creating my $cmd = UR::Namespace::Command::Define::Namespace->create(nsname => 'NewNamespace'); ok($cmd, 'create UR::Namespace::Command::Define::Namespace'); $cmd->dump_status_messages(0); $cmd->dump_error_messages(0); $cmd->dump_warning_messages(0); $cmd->queue_status_messages(1); $cmd->queue_error_messages(1); $cmd->queue_warning_messages(1); ok($cmd->execute, 'execute'); my $namespace = UR::Namespace->get('NewNamespace'); ok($namespace, 'Namespace object created'); my $data_source = UR::DataSource->get('NewNamespace::DataSource::Meta'); ok($data_source, 'Metadata data source object created'); ok(-f 'NewNamespace.pm', 'NewNamespace.pm module exists'); ok(-d 'NewNamespace', 'NewNamespace directory exists'); ok(-d 'NewNamespace/DataSource', 'NewNamespace/DataSource directory exists'); ok(-f 'NewNamespace/DataSource/Meta.pm', 'NewNamespace/DataSource/Meta.pm module exists'); ok(-f 'NewNamespace/Vocabulary.pm', 'NewNamespace/Vocabulary.pm module exists'); my @messages = $cmd->status_messages(); is($messages[0], 'A NewNamespace (UR::Namespace)', 'Message adding NewNamespace'); is($messages[1], 'A NewNamespace::Vocabulary (UR::Vocabulary)', 'Message adding vocabulary'); is($messages[2], 'A NewNamespace::DataSource::Meta (UR::DataSource::Meta)', 'Message adding meta datasource'); like($messages[3], qr(A /.+/NewNamespace/DataSource/Meta\.sqlite3n?-dump [(]Metadata DB skeleton[)]), 'Message adding metaDB dump file'); ur-cachetest.pl100664023532023421 372412544604517 15033 0ustar00abrummetgsc000000000000UR-0.44/tuse strict; use warnings; use Time::HiRes; my $n_props = shift(@ARGV) || 5; my $lw = shift(@ARGV); my $hw = shift(@ARGV); $ENV{UR_CONTEXT_CACHE_SIZE_LOWWATER} = $lw; $ENV{UR_CONTEXT_CACHE_SIZE_HIGHWATER} = $hw; $ENV{UR_DEBUG_OBJECT_PRUNING} = 1; $ENV{UR_DEBUG_OBJECT_RELEASE} = 1; print STDERR "using classes with $n_props properties\n"; print STDERR "low/high water is $lw/$hw\n"; my @pnames = map { "p$_" } (1..$n_props-1); ## require UR; class UrObj { has => [@pnames] }; sub UrObj::__load__ { # an infinite data set (will hang if you don't iterate) my $data = IO::File->new("perl -e 'my \$id = 1; while(1) { print \$id++,qq|\n| }' |"); my $iterator = sub { my $v = $data->getline; chomp $v; if (not defined $v or $v == $n) { $data->close(); return; } return [$v,$v,$v,$v]; }; return (['id',@pnames], $iterator); } ## package main; my $i = UrObj->create_iterator(); my $n = 0; while ($o = $i->next) { $n++; if ($n % 10_000 == 0) { my @o = UrObj->is_loaded(); my $loaded = scalar(@o); @o = (); print STDERR UR::Context->now, ":\t$n objects, with $loaded loaded\n"; } if ($n == 2_000_010) { last; } } __END__ my @a; $#a = $n; my @x; my $prev_d; for my $t (@t) { my $t1 = Time::HiRes::time(); my $o; my @o; my $s = 'sub { push @a, ' . $t . "}"; print $s,"\n"; my $f = eval $s; die "$@" if $@; if (substr($t,0,1) ne '@') { #print "each...\n"; for (1..$n) { for my $p (@pvalues) { $p = $_ }; @p{@pnames} = @pvalues; $f->(%p); }; } else { #print "bulk...\n"; $f->(); } my $d = Time::HiRes::time()-$t1; my $diff = ($prev_d ? $d/$prev_d : 0); $prev_d = $d; print "$d seconds for $n of: $t\n ...$diff x slower than the prior\n\n"; } package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; urbenchmark.pl100664023532023421 515512544604517 14745 0ustar00abrummetgsc000000000000UR-0.44/tuse strict; use warnings; use Time::HiRes; my $n_props = shift(@ARGV) || 5; my $n = shift(@ARGV) || 100_000; print STDERR "using classes with $n_props properties\n"; print STDERR "testing on $n objects\n\n"; my @pnames = map { "p$_" } (1..$n_props-1); package UrObj; use UR; class UrObj { has => [@pnames] }; # simulate a million-item table with "1" in each column sub __load__ { my $data = IO::File->new("perl -e 'my \$id = 1; while(\$id <= $n) { print \$id++,qq|\n| }' |"); my $iterator = sub { my $v = $data->getline; chomp $v; if (not defined $v or $v == $n) { $data->close(); return; } return [$v,$v,$v,$v]; }; return (['id',@pnames], $iterator); } package MooseObj; use Moose; has 'id' => (is => 'ro'); for (@pnames) { has $_ => (is => 'rw'); } push @pnames, 'id'; my @pvalues; $#pvalues = $#pnames; my %p; package main; my @t = ( '$o = bless({ @_ },"PerlObj")', #'$o = UR::BoolExpr->resolve("UrObj",@_)', #'$o = bless({ @{ UR::BoolExpr->resolve("UrObj",@_)->{_params_list} } } , "UrObj")', #'$o = bless({ UR::BoolExpr->resolve("UrObj",@_)->_params_list } , "UrObj")', #'do { my $b = UR::BoolExpr->resolve("UrObj",@_); my @p = $b->_params_list; my @pp = $b->template->extend_params_list_for_values(@p); bless({@p, @pp}, "UrObj"); };', #'$o = UR::BoolExpr->resolve_normalized("UrObj",@_)', #'$o = bless({ UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list } , "UrObj")', #'$o = bless({ UR::BoolExpr->resolve_normalized("UrObj",@_)->params_list}, "UrObj")', '@o = UrObj->get()', '$o = UrObj->create(@_)', '$o = MooseObj->new(@_)', #'do { @x = UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list; $o = bless { @x, db_committed => { @x } } , "UrObj"; }; ', #'do { @x = UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list; $o = bless { @x } , "UrObj"; }; ', ); my @a; $#a = $n; my @x; my $prev_d; for my $t (@t) { my $t1 = Time::HiRes::time(); my $o; my @o; my $s = 'sub { push @a, ' . $t . "}"; print $s,"\n"; my $f = eval $s; die "$@" if $@; if (substr($t,0,1) ne '@') { #print "each...\n"; for (1..$n) { for my $p (@pvalues) { $p = $_ }; @p{@pnames} = @pvalues; $f->(%p); }; } else { #print "bulk...\n"; $f->(); } my $d = Time::HiRes::time()-$t1; my $diff = ($prev_d ? $d/$prev_d : 0); $prev_d = $d; print "$d seconds for $n of: $t\n ...$diff x slower than the prior\n\n"; } package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; changelog100664023532023421 7052112544604517 16155 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucidlibur-perl (0.44-1) unstable; urgency=low * Added UR::Context::AutoUnloadPool - a mechanism for automatically unloading objects when a leaving a scope. * Added methods to UR::Object::Type to introspect methods names relating to is_many properties. * The MetaDB no longer tracks owner/schema. Classes using tables not in the default schema should have their table_name listed as "schema.table". * Added copy() constructor to UR::Object * Removed old, deprecated filter parser for turning text into a UR::BoolExpr within UR::Object::Command::List * Meta-params like -order_by are now allowed in a delegated property's where clase. * Retrieving values for doubly-delegated properties is more efficient. * An id-by delegated property can point to a class with multiple ID properties. The linking value is the composite ID. * Observers now have a 'once' property. Setting it to true ensures the callback will only ever fire one time. The Observer is deleted. * Properties can have a 'calculated_default' subref. Works like default_value, but the return value from the subref is the default value rather than have a hardcoded default. * Fixes to work with newer versions of SQLite Added __rollback__() to UR::Object, called when the base Context rolls-back. Subclasses can override this to provide special behavior during rollback. The override should also call SUPER::__rollback__(). -- Anthony Brummett Fri, 19 Jun 2015 09:30:00 -600 libur-perl (0.43-1) unstable; urgency=low * Set objects now have member_iterator() method. * Data loaded from an RDBMS during the life of a program can be copied to an alternate database. * Use the "C" collation with PostgreSQL when doing an order-by on a text-type column to match how UR will sort cached objects using perl's cmp. * Singleton accessors can be called on the class as well as the instance. * Class initializer is more strict about what is a valid property name; it must be a valid function name. * Added UR::Value::JSON class. Its "id" is a JSON-encoded string of the instance's properties and values. * Added UR::Context::Transaction::eval() and do() functions to wrap software transactions around blocks. * Added UR::DataSource::RDBMSRetriableOperations mixin class to allow RDBMSs to control whether failed DB operations should be retried. * Added signals for when a data source fails a query or commit, and when the handle is created or disconnected. * Added signal to UR::Context for when synchronizing to the datasources has succeeded or failed. * Added 'isa' operator to boolean expressions. Evaluates to true if the attribute isa the given class. * Fixed a bug where a Set object's value for an aggregate would be incorrect if cached member objects' values change. * Fixed a bug where UR objects frozen in boolean expressions could cause database rows to be deleted when thawed. * UR's class browser (ur sys class-browser) is working again. -- Anthony Brummett Thu, 3 Jul 2014 13:08:00 -600 libur-perl (0.42-1) unstable; urgency=low * Test releases to try out our new release system with minilla -- Anthony Brummett Thu, 26 Jun 2014 13:08:00 -600 libur-perl (0.41-1) unstable; urgency=low * above.pm now imports symbols into the caller's package * Fix for database connections after fork() in the child process * Fixes for command-line parsing, implied property metadata and database joins * Many test updates to work on more architectures -- Anthony Brummett Mon, 18 Mar 2013 13:08:00 -600 libur-perl (0.40-1) unstable; urgency=low * RDBMS data sources now have infrastructure for comparing text and non-text columns during a join. When a number or date column is joined with a text column, the non-text column is converted with the to_char() function in the Oracle data source. * An object-type property's default_value can now be specified using a hashref of keys/values. * Property definitions can now include example_values - a listref of values shown to the user in the autogenerated documentation. * Documentation for the Object Lister base command is expanded. -- Anthony Brummett Mon, 25 Feb 2013 11:00:00 -0500 libur-perl (0.392-1) unstable; urgency=low * Changed the name for the Yapp driver package to avoid a CPAN warning about unauthorized use of their namespace -- Anthony Brummett Thu, 31 Jan 2013 15:15:00 -0500 libur-perl (0.39-1) unstable; urgency=low * Better support for PostgreSQL. It is now on par with Oracle. * New datasource UR::DataSource::Filesystem. It obsoletes UR::DataSource::File and UR::DataSource::FileMux, and is more flexible. * Classes can specify a query hint when they are used as the primary class of a get() or when they are involved in a join. * BoolExprs with an or-clause now support hints and order-by correctly.. * Messaging methods (error_message(), status_message(), etc) now trigger observers of the same name. This means any number of message observers can be attached at any point in the class hierarchy. * Using chained delegated properties with the dot-syntax (object.deledate.prop) is accepted in more places. * Better support for queries using direct SQL. * Many fixes for the Boolean Expression syntax parser. Besides fixing bugs, it now supports more operators and understands 'offset' and 'limit'. * Support for defining a property that is an alias for another. * Fixes for remaining connected to databases after fork(). * Optimization for the case where a delegation goes through an abstract class with no data source and back to the original data source. It now does one query instead of many. * Improvements to the Command API documentation. * Removed some deps on XML-related modules. -- Scott Smith Wed, 30 Jan 2013 20:17:00 -0500 libur-perl (0.38-1) unstable; urgency=low * Bug fixes to support C3 inheritance on the Mac correctly. * Rich extensions to primitive/value data-types for files, etc. * Optimization for very large in-clauses. * Database updates now infer table structure from class meta-data instead of leaning on database metadata when inserting (update and delete already do this). * Bug fixes to the new boolean expression parser. * Fixes to complex inheritance in RDBMS data. * Fix to sorting issues in older Perl 5.8. * Bug fixes to boolean expressions with values which are non-UR objects * Smarter query plans when the join table is variable (not supported in SQL, but in the API), leading to multiple database queries where necessary. -- Scott Smith Wed, 28 Mar 2012 14:24:00 -0500 libur-perl (0.37-1) unstable; urgency=low * Added a proper parser for generating Boolean Expressions from text strings. See the UR::BoolExpr pod for more info. * Or-type Boolean Expressions now support -order, and can be the filter for iterators. * Better error messages when a module fails to load properly during autoloading. * Class methods called on Set instances are dispatched to the proper class instead of called on the Set's members. * Values in an SQL in-clause are escaped using DBI's quote() method. -- Anthony Brummett Fri, 03 Feb 2012 15:40:00 -0500 libur-perl (0.36-1) unstable; urgency=low * Fix for 'like' clause's escape string on PostgreSQL * Speed improvement for class initialization by normalizing metadata more efficiently and only calculating the cached data for property_meta_for_name() once. * Workaround for a bug in Perl 5.8 involving sorters by avoiding method calls inside some sort subs * Fully deprecate the old Subscription API in favor of the new Observer api * UR::Value classes use UR::DataSource::Default and the normal loading mechanism. Previously, UR::Values used a special codepath to get loaded into memory * Add a json perspective for available views * Allow descending sorts in order-by * Standardize sorting results on columns with NULLs by having NULL/undef always appears at the end for ascending sorts. Previously, the order depended on the data source's behavior. Oracle and PostgreSQL put them at the end, while MySQL, SQLite and cached get()s put them at the beginnind. * Fix exit code for 'ur test run' when the --lsf arg is used. It used always return a false value (1). Now it returns true (0) if all tests pass, and false (1) if any one test fails. * UR::Object now implements the messaging API that used to be in Command (error_message, dump_error_messages, etc). The old messaging API is now deprecated. -- Anthony Brummett Thu, 05 Jan 2012 15:40:00 -0500 libur-perl (0.35-1) unstable; urgency=low * Queries with the -recurse option are suppored for all datasources, not just those that support recursive queries directly * Make the object listers more user-friendly by implicitly putting '%' wildcards on either side of the user-supplied 'like' filter * Update to the latest version of Getopt::Complete for command-line completion * Object Set fixes (non-datasource expressable filters) * Bugfixes for queries involving multiple joins to the same table with different join conditions * Queries with -offset/-limit and -page are now supported. * Query efficiency improvements * Command module fixes * Deleted objects hanging around as UR::DeletedRefs are recycled if the original object gets re-created -- Anthony Brummett Fri, 28 Oct 2011 12:40:00 -0500 libur-perl (0.34-1) unstable; urgency=low * New class (Command::SubCommandFactory) which can act as a factory for a tree of sub-commands * Remove the distinction between older and newer versions of DBD::SQLite installed on the system. If you have SQLite databases (including MetaDBs) with names like "*sqlite3n*", they will need to be renamed to "*sqlite3*". * Make the tests emit fewer messages to the terminal when run in the harness; improve coverage on non-Intel/Linux systems. -- Anthony Brummett Thu, 26 Jul 2011 10:25:00 -0500 libur-perl (0.33-1) unstable; urgency=low * New environment variable (UR_DBI_SUMMARIZE_SQL) to help find query optimization targets * View aspects for objects' primitive values use the appropriate UR::Value View classes * Query engine remembers cases where a left join matches nothing, and skips asking the datasource on subsequent similar queries * Committing a software transaction now performs the same data consistancy checking as the top-level transaction. * Improved document auto-generation for Command classes * Improved SQLite Data Source schema introspection * Updated database handling for Pg and mysql table case sensitivity * UR's developer tools (ur command-line tool) can operate on non-standard source tree layouts, and can be forced to operate on a namespace with a command-line option * Support for using a chain of properties in queries ('a.b.c like' => $v) * Set operations normalized: min, max, sum, count * Set-to-set relaying is now correctly lazy * Objects previously loaded from the database, and later deleted from the database, are now detected as deleted and handled as another type of change to be merged with in-memory changes. -- Anthony Brummett Thu, 30 Jun 2011 17:47:00 -0500 libur-perl (0.32-1) unstable; urgency=low * Internal-only release -- Anthony Brummett Wed, 29 Jun 2011 12:00:00 -0500 libur-perl (0.31-1) unstable; urgency=low * Internal-only release -- Anthony Brummett Sat, 28 May 2011 09:26:41 -0500 libur-perl (0.30-1) unstable; urgency=low * Latest build -- Anthony Brummett Wed, 20 Apr 2011 09:26:41 -0500 libur-perl (0.29-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 23 Jan 2011 00:00:01 -0500 libur-perl (0.28-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.27-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.26-1) unstable; urgency=low * yet another refactoring to ensure VERSION appears on all modules * fixes for tests which fail only in the install harness -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.25-1) unstable; urgency=low * Updated docs. -- Anthony Brummett Fri, 15 Jan 2011 00:00:01 -0500 libur-perl (0.24-1) unstable; urgency=low * Updated deps to compile fully on a new OSX installation (requires XCode). -- Anthony Brummett Fri, 15 Jan 2011 00:00:01 -0500 libur-perl (0.22-1) unstable; urgency=low * VERSION refactoring for cleaner uploads -- Anthony Brummett Fri, 12 Jan 2011 00:00:01 -0500 libur-perl (0.20-1) unstable; urgency=low * faster compile (<.5s) * faster object creation * faster install * documentation polish -- Anthony Brummett Fri, 1 Jan 2011 00:00:01 -0500 libur-perl (0.19-1) unstable; urgency=low * faster compile * faster query cache resolution * leaner meta-data * less build deps, build dep fixes * hideable commands * fixes for newer sqlite API * revamped UR::BoolExpr API * new command tree -- Anthony Brummett Fri, 24 Dec 2010 00:00:01 -0500 libur-perl (0.18-1) unstable; urgency=low * Bugfix for queries involving subclasses without tables * Preliminary support for building debian packages * Bugfixes for queries with the 'in' and 'not in' operators * Object cache indexing sped up by replacing regexes with direct string comparisons -- Anthony Brummett Fri, 10 Dec 2010 00:00:01 -0500 libur-perl (0.17-1) unstable; urgency=low * Fixed bug with default datasources dumping debug info during queries. * Deprecated old parts of the UR::Object API. * Bugfixes for MySQL data sources with handling of between and like operators, and table/column name case sensitivity * MySQL data sources will complain if the 'lower_case_table_names' setting is not set to 1 * Bugfixes for FileMux data sources to return objects from iterators in correct sorted order * File data sources remember their file offsets more often to improve seeking * Bugfixes for handling is_many values passed in during create() * New class for JSON-formatted Set views * More consistent behavior during evaluation of BoolExprs with is_many values and undef/NULL values * Bugfixes for handling observers during software transaction commit and rollback * Addition of a new UR::Change type (external_change) to track non-UR entities that need undo-ing during a rollback -- Anthony Brummett Wed, 10 Nov 2010 00:00:01 -0500 libur-perl (0.16-1) unstable; urgency=low * File datasources build an on-the-fly index to improve its ability to seek within the file * Initial support for classes to supply custom logic for loading data * Compile-time speed improvements * Bug fixes for SQL generation with indirect properties, and the object cache pruner -- Anthony Brummett Mon, 27 Sep 2010 00:00:01 -0500 libur-perl (0.15-2) unstable; urgency=low * Add Debian packaging. -- Matt Callaway Mon, 6 Sep 2010 00:05:47 -0500 libur-perl (0.15-1) unstable; urgency=low * Improved 'ur update classes' interaction with MySQL databases * Integration with Getopt::Complete for bash command-line tab completion -- Anthony Brummett Tue, 3 Aug 2010 00:00:01 -0500 libur-perl (0.14-1) unstable; urgency=low * Metadata about data source entities (tables, columns, etc) is autodiscovered within commit() if it doesn't already exist in the MetaDB * The new View API now has working default toolkits for HTML, Text, XML and XSL. The old Viewer API has been removed. * Smarter property merging when the Context reloads an already cached object and the data in the data source has changed * Added a built-in 'product' calculation property type * Calculated properties can now be memoized * subclassify_by for an abstract class can now be a regular, indirect or calculated property * New environment variable UR_CONTEXT_MONITOR_QUERY for printing Context/query info to stdout * SQLite data sources can initialize themselves even if the sqlite3 executable cannot be found * Test harness improvements: --junit and --color options, control-C stops tests and reports results 'use lib' within an autoloaded module stays in effect after the module is loaded -- Anthony Brummett Mon, 26 Jul 2010 00:00:01 -0500 libur-perl (0.13-1) unstable; urgency=low * Circular foreign key constraints between tables are now handled smartly handled in UR::DataSource::RDBMS. * New meta-property properties: id_class_by, order_by, specify_by. * Updated autogenerated Command documentation. * Formalized the __extend_namespace__ callback for dynamic class creation. * New Command::DynamicSubCommands class makes command trees for a group of classes easy. * The new view API is available. The old "viewer" API is still available in this release, but is deprecated. -- Anthony Brummett Sun, 21 Feb 2010 00:00:01 -0500 libur-perl (0.12-1) unstable; urgency=low * 'ur test run' can now run tests in parallel and can submit tests as jobs to LSF * Command modules now have support for Getopt::Complete for bash tab-completion * Bugfixes related to saving objects to File data sources. * Several more fixes for merging between database and in-memory objects. * Property names beginning with an underscore are now handled properly during rule * and object creation -- Anthony Brummett Wed, 09 Sep 2009 00:00:01 -0500 libur-perl (0.11-1) unstable; urgency=low * Fix bug in merge between database/in-memory data sets with changes. -- Anthony Brummett Thu, 30 Jul 2009 00:00:01 -0500 libur-perl (0.10-1) unstable; urgency=low * Updates to the UR::Object::Type MOP documentation. * Other documentation cleanup and file cleanup. -- Anthony Brummett Wed, 22 Jul 2009 00:00:01 -0500 libur-perl (0.09-1) unstable; urgency=low * Additional build fixes. -- Anthony Brummett Fri, 19 Jun 2009 00:00:01 -0500 libur-perl (0.08-1) unstable; urgency=low * David's build fixes. -- Anthony Brummett Wed, 17 Jun 2009 00:00:01 -0500 libur-perl (0.07-1) unstable; urgency=low * Fix to build process: the distribution will work if you do not have Module::Install installed. -- Anthony Brummett Wed, 10 Jun 2009 00:00:01 -0500 libur-perl (0.06-1) unstable; urgency=low * Fixed to build process: actually install the "ur" executable. -- Anthony Brummett Sun, 07 Jun 2009 00:00:01 -0500 libur-perl (0.05-1) unstable; urgency=low * Updates to POD. Additional API updates to UR::Object. -- Anthony Brummett Sat, 06 Jun 2009 00:00:01 -0500 libur-perl (0.04-1) unstable; urgency=low * Updates to POD. Extensive API updates to UR::Object. -- Anthony Brummett Fri, 04 Jun 2009 00:00:01 -0500 libur-perl (0.03-1) unstable; urgency=low * Fixed memory leak in cache pruner, and added additional debugging environment variable. * Additional laziness on file-based data-sources. * Updated lots of POD. * Switched to version numbers without zero padding! -- Anthony Brummett Fri, 29 May 2009 00:00:01 -0500 libur-perl (0.02-1) unstable; urgency=low * Cleanup of initial deployment issues. * UR uses a non-default version of Class::Autouse. This is now a special file to prevent problems with the old version. * Links to old DBIx::Class modules are now gone. * Updated boolean expression API. -- Anthony Brummett Sat, 23 May 2009 00:00:01 -0000 libur-perl (0.01-1) unstable; urgency=low * First public release for Lambda Lounge language shootout. -- Anthony Brummett Thu, 7 May 2009 00:00:01 -0000 libur-perl (0.29-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 23 Jan 2011 00:00:01 -0500 libur-perl (0.28-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.27-1) unstable; urgency=low * Latest build -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.26-1) unstable; urgency=low * yet another refactoring to ensure VERSION appears on all modules * fixes for tests which fail only in the install harness -- Anthony Brummett Sun, 16 Jan 2011 00:00:01 -0500 libur-perl (0.25-1) unstable; urgency=low * Updated docs. -- Anthony Brummett Fri, 15 Jan 2011 00:00:01 -0500 libur-perl (0.24-1) unstable; urgency=low * Updated deps to compile fully on a new OSX installation (requires XCode). -- Anthony Brummett Fri, 15 Jan 2011 00:00:01 -0500 libur-perl (0.22-1) unstable; urgency=low * VERSION refactoring for cleaner uploads -- Anthony Brummett Fri, 12 Jan 2011 00:00:01 -0500 libur-perl (0.20-1) unstable; urgency=low * faster compile (<.5s) * faster object creation * faster install * documentation polish -- Anthony Brummett Fri, 1 Jan 2011 00:00:01 -0500 libur-perl (0.19-1) unstable; urgency=low * faster compile * faster query cache resolution * leaner meta-data * less build deps, build dep fixes * hideable commands * fixes for newer sqlite API * revamped UR::BoolExpr API * new command tree -- Anthony Brummett Fri, 24 Dec 2010 00:00:01 -0500 libur-perl (0.18-1) unstable; urgency=low * Bugfix for queries involving subclasses without tables * Preliminary support for building debian packages * Bugfixes for queries with the 'in' and 'not in' operators * Object cache indexing sped up by replacing regexes with direct string comparisons -- Anthony Brummett Fri, 10 Dec 2010 00:00:01 -0500 libur-perl (0.17-1) unstable; urgency=low * Fixed bug with default datasources dumping debug info during queries. * Deprecated old parts of the UR::Object API. * Bugfixes for MySQL data sources with handling of between and like operators, and table/column name case sensitivity * MySQL data sources will complain if the 'lower_case_table_names' setting is not set to 1 * Bugfixes for FileMux data sources to return objects from iterators in correct sorted order * File data sources remember their file offsets more often to improve seeking * Bugfixes for handling is_many values passed in during create() * New class for JSON-formatted Set views * More consistent behavior during evaluation of BoolExprs with is_many values and undef/NULL values * Bugfixes for handling observers during software transaction commit and rollback * Addition of a new UR::Change type (external_change) to track non-UR entities that need undo-ing during a rollback -- Anthony Brummett Wed, 10 Nov 2010 00:00:01 -0500 libur-perl (0.16-1) unstable; urgency=low * File datasources build an on-the-fly index to improve its ability to seek within the file * Initial support for classes to supply custom logic for loading data * Compile-time speed improvements * Bug fixes for SQL generation with indirect properties, and the object cache pruner -- Anthony Brummett Mon, 27 Sep 2010 00:00:01 -0500 libur-perl (0.15-2) unstable; urgency=low * Add Debian packaging. -- Matt Callaway Mon, 6 Sep 2010 00:05:47 -0500 libur-perl (0.15-1) unstable; urgency=low * Improved 'ur update classes' interaction with MySQL databases * Integration with Getopt::Complete for bash command-line tab completion -- Anthony Brummett Tue, 3 Aug 2010 00:00:01 -0500 libur-perl (0.14-1) unstable; urgency=low * Metadata about data source entities (tables, columns, etc) is autodiscovered within commit() if it doesn't already exist in the MetaDB * The new View API now has working default toolkits for HTML, Text, XML and XSL. The old Viewer API has been removed. * Smarter property merging when the Context reloads an already cached object and the data in the data source has changed * Added a built-in 'product' calculation property type * Calculated properties can now be memoized * subclassify_by for an abstract class can now be a regular, indirect or calculated property * New environment variable UR_CONTEXT_MONITOR_QUERY for printing Context/query info to stdout * SQLite data sources can initialize themselves even if the sqlite3 executable cannot be found * Test harness improvements: --junit and --color options, control-C stops tests and reports results 'use lib' within an autoloaded module stays in effect after the module is loaded -- Anthony Brummett Mon, 26 Jul 2010 00:00:01 -0500 libur-perl (0.13-1) unstable; urgency=low * Circular foreign key constraints between tables are now handled smartly handled in UR::DataSource::RDBMS. * New meta-property properties: id_class_by, order_by, specify_by. * Updated autogenerated Command documentation. * Formalized the __extend_namespace__ callback for dynamic class creation. * New Command::DynamicSubCommands class makes command trees for a group of classes easy. * The new view API is available. The old "viewer" API is still available in this release, but is deprecated. -- Anthony Brummett Sun, 21 Feb 2010 00:00:01 -0500 libur-perl (0.12-1) unstable; urgency=low * 'ur test run' can now run tests in parallel and can submit tests as jobs to LSF * Command modules now have support for Getopt::Complete for bash tab-completion * Bugfixes related to saving objects to File data sources. * Several more fixes for merging between database and in-memory objects. * Property names beginning with an underscore are now handled properly during rule * and object creation -- Anthony Brummett Wed, 09 Sep 2009 00:00:01 -0500 libur-perl (0.11-1) unstable; urgency=low * Fix bug in merge between database/in-memory data sets with changes. -- Anthony Brummett Thu, 30 Jul 2009 00:00:01 -0500 libur-perl (0.10-1) unstable; urgency=low * Updates to the UR::Object::Type MOP documentation. * Other documentation cleanup and file cleanup. -- Anthony Brummett Wed, 22 Jul 2009 00:00:01 -0500 libur-perl (0.09-1) unstable; urgency=low * Additional build fixes. -- Anthony Brummett Fri, 19 Jun 2009 00:00:01 -0500 libur-perl (0.08-1) unstable; urgency=low * David's build fixes. -- Anthony Brummett Wed, 17 Jun 2009 00:00:01 -0500 libur-perl (0.07-1) unstable; urgency=low * Fix to build process: the distribution will work if you do not have Module::Install installed. -- Anthony Brummett Wed, 10 Jun 2009 00:00:01 -0500 libur-perl (0.06-1) unstable; urgency=low * Fixed to build process: actually install the "ur" executable. -- Anthony Brummett Sun, 07 Jun 2009 00:00:01 -0500 libur-perl (0.05-1) unstable; urgency=low * Updates to POD. Additional API updates to UR::Object. -- Anthony Brummett Sat, 06 Jun 2009 00:00:01 -0500 libur-perl (0.04-1) unstable; urgency=low * Updates to POD. Extensive API updates to UR::Object. -- Anthony Brummett Fri, 04 Jun 2009 00:00:01 -0500 libur-perl (0.03-1) unstable; urgency=low * Fixed memory leak in cache pruner, and added additional debugging environment variable. * Additional laziness on file-based data-sources. * Updated lots of POD. * Switched to version numbers without zero padding! -- Anthony Brummett Fri, 29 May 2009 00:00:01 -0500 libur-perl (0.02-1) unstable; urgency=low * Cleanup of initial deployment issues. * UR uses a non-default version of Class::Autouse. This is now a special file to prevent problems with the old version. * Links to old DBIx::Class modules are now gone. * Updated boolean expression API. -- Anthony Brummett Sat, 23 May 2009 00:00:01 -0000 libur-perl (0.01-1) unstable; urgency=low * First public release for Lambda Lounge language shootout. -- Anthony Brummett Thu, 7 May 2009 00:00:01 -0000 compat100664023532023421 212544604517 15374 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucid7 control100664023532023421 363612544604517 15671 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucidSource: libur-perl Section: perl Priority: optional Build-Depends: debhelper (>= 7), perl (>= 5.8.0) Build-Depends-Indep: perl (>= 5.8.0),libtest-fork-perl (>= 0.02),libmodule-build-perl,libtest-simple-perl,liblingua-en-inflect-perl,libfreezethaw-perl,libdate-pcalc-perl,libyaml-perl,libdata-compare-perl,libtext-diff-perl,libdbi-perl,libsub-name-perl,libsub-install-perl,libpath-class-perl,libxml-dumper-perl,libxml-generator-perl,libxml-libxml-perl,libtimedate-perl,libdbd-sqlite3-perl,libclone-pp-perl (>= 1.02),libclass-autoloadcan-perl,libclass-autouse-perl (>= 2.00),libgetopt-complete-perl (>= 0.20),libnet-httpserver-perl,libxml-simple-perl,libclass-inspector-perl,libjson-perl,libossp-uuid-perl,libmro-compat-perl,libplack-perl Maintainer: Anthony Brummett Standards-Version: 3.8.3 Homepage: http://search.cpan.org/dist/UR/ Package: libur-perl Provides: ur Architecture: all Depends: ${perl:Depends}, ${misc:Depends}, perl (>= 5.8.0),liblingua-en-inflect-perl,libfreezethaw-perl,libdate-pcalc-perl,libyaml-perl,libdata-compare-perl,libtext-diff-perl,libdbi-perl,libsub-name-perl,libsub-install-perl,libpath-class-perl,libxml-dumper-perl,libxml-generator-perl,libxml-libxml-perl,libtimedate-perl,libdbd-sqlite3-perl,libclone-pp-perl (>= 1.02),libclass-autoloadcan-perl,libclass-autouse-perl (>= 2.00),libnet-httpserver-perl,libjson-perl,libossp-uuid-perl,libpod-simple-perl (>= 3.13), libplack-perl Suggests: libgetopt-complete-perl (>= 0.20),libxml-xslt-perl,libmro-compat-perl Description: Rich declarative transactional objects UR is a class framework and object/relational mapper for Perl. It starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and extends its capabilities with ORM (object-relational mapping) capabilities, object cache, in-memory transactions, more formal class definitions, metadata, documentation system, iterators, command line tools, etc. copyright100664023532023421 264412544604517 16217 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucidFormat-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 Maintainer: UR was built by the software development team at the McDonnell Genome Institute Source: http://search.cpan.org/dist/UR/ Name: UR DISCLAIMER: This copyright info was automatically extracted from the perl module. It may not be accurate, so you better check the module sources in order to ensure the module for its inclusion in Debian or for general legal information. Please, if licensing information is incorrectly generated, file a bug on dh-make-perl. NOTE: Don't forget to remove this disclaimer once you are happy with this file. Files: * Copyright: UR was built by the software development team at the McDonnell Genome Institute License: Artistic or GPL-3+ License: Artistic This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License, which comes with Perl. . On Debian GNU/Linux systems, the complete text of the Artistic License can be found in `/usr/share/common-licenses/Artistic' License: GPL-3+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. . On Debian GNU/Linux systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL-3' rules100775023532023421 3612544604517 15275 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucid#!/usr/bin/make -f %: dh $@ watch100664023532023421 14112544604517 15263 0ustar00abrummetgsc000000000000UR-0.44/ubuntu-lucidversion=3 http://search.cpan.org/dist/UR/ .*/UR-v?(\d[\d.-]+)\.(?:tar(?:\.gz|\.bz2)?|tgz|zip)$ META.yml100664023532023421 7765212544604517 13150 0ustar00abrummetgsc000000000000UR-0.44--- abstract: 'rich declarative transactional objects' author: - 'UR was built by the software development team at the McDonnell Genome' build_requires: Test::Deep: '0' Test::Exception: '0' Test::Fatal: '0' Test::Fork: '0' Test::More: '0.98' configure_requires: CPAN::Meta: '0' CPAN::Meta::Prereqs: '0' Module::Build: '0.38' dynamic_config: 0 generated_by: 'Minilla/v1.1.0, CPAN::Meta::Converter version 2.141170' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: UR no_index: directory: - t - xt - inc - share - eg - examples - author - builder provides: Command: file: lib/Command.pm version: '0.44' Command::Common: file: lib/Command/Common.pm Command::DynamicSubCommands: file: lib/Command/DynamicSubCommands.pm Command::Shell: file: lib/Command/Shell.pm Command::SubCommandFactory: file: lib/Command/SubCommandFactory.pm Command::Test: file: lib/Command/Test.pm Command::Test::Echo: file: lib/Command/Test/Echo.pm Command::Test::Tree1: file: lib/Command/Test/Tree1.pm Command::Test::Tree1::Echo1: file: lib/Command/Test/Tree1/Echo1.pm Command::Test::Tree1::Echo2: file: lib/Command/Test/Tree1/Echo2.pm Command::Tree: file: lib/Command/Tree.pm version: '0.44' Command::V1: file: lib/Command/V1.pm version: '0.44' Command::V2: file: lib/Command/V2.pm version: '0.44' Devel::callsfrom: file: lib/Devel/callcount.pm My::TAP::Parser::Iterator::Process::LSF: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::IteratorFactory::LSF: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Multiplexer: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Scheduler: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Timer: file: lib/UR/Namespace/Command/Test/Run.pm UR: file: lib/UR.pm version: '0.44' UR::All: file: lib/UR/All.pm version: '0.44' UR::BoolExpr: file: lib/UR/BoolExpr.pm version: '0.44' UR::BoolExpr::BxParser: file: lib/UR/BoolExpr/BxParser.pm UR::BoolExpr::BxParser::Yapp::Driver: file: lib/UR/BoolExpr/BxParser.pm version: '1.05' UR::BoolExpr::Template: file: lib/UR/BoolExpr/Template.pm version: '0.44' UR::BoolExpr::Template::And: file: lib/UR/BoolExpr/Template/And.pm version: '0.44' UR::BoolExpr::Template::Composite: file: lib/UR/BoolExpr/Template/Composite.pm version: '0.44' UR::BoolExpr::Template::Or: file: lib/UR/BoolExpr/Template/Or.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison: file: lib/UR/BoolExpr/Template/PropertyComparison.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::Between: file: lib/UR/BoolExpr/Template/PropertyComparison/Between.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::Equals: file: lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::False: file: lib/UR/BoolExpr/Template/PropertyComparison/False.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual: file: lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::GreaterThan: file: lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::In: file: lib/UR/BoolExpr/Template/PropertyComparison/In.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::Isa: file: lib/UR/BoolExpr/Template/PropertyComparison/Isa.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::LessOrEqual: file: lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::LessThan: file: lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::Like: file: lib/UR/BoolExpr/Template/PropertyComparison/Like.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::Matches: file: lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::NotBetween: file: lib/UR/BoolExpr/Template/PropertyComparison/NotBetween.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::NotEquals: file: lib/UR/BoolExpr/Template/PropertyComparison/NotEquals.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::NotIn: file: lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::NotLike: file: lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm version: '0.44' UR::BoolExpr::Template::PropertyComparison::True: file: lib/UR/BoolExpr/Template/PropertyComparison/True.pm version: '0.44' UR::BoolExpr::Util: file: lib/UR/BoolExpr/Util.pm version: '0.44' UR::BoolExpr::Util::clonedThing: file: lib/UR/BoolExpr/Util.pm UR::Change: file: lib/UR/Change.pm version: '0.44' UR::Context: file: lib/UR/Context.pm version: '0.44' UR::Context::AutoUnloadPool: file: lib/UR/Context/AutoUnloadPool.pm version: '0.44' UR::Context::DefaultRoot: file: lib/UR/Context/DefaultRoot.pm version: '0.44' UR::Context::LoadingIterator: file: lib/UR/Context/LoadingIterator.pm version: '0.44' UR::Context::ObjectFabricator: file: lib/UR/Context/ObjectFabricator.pm version: '0.44' UR::Context::Process: file: lib/UR/Context/Process.pm version: '0.44' UR::Context::Root: file: lib/UR/Context/Root.pm version: '0.44' UR::Context::Transaction: file: lib/UR/Context/Transaction.pm version: '0.44' UR::DBI: file: lib/UR/DBI.pm version: '0.44' UR::DBI::Report: file: lib/UR/DBI/Report.pm version: '0.44' UR::DBI::db: file: lib/UR/DBI.pm UR::DBI::st: file: lib/UR/DBI.pm UR::DataSource: file: lib/UR/DataSource.pm version: '0.44' UR::DataSource::CSV: file: lib/UR/DataSource/CSV.pm version: '0.44' UR::DataSource::Code: file: lib/UR/DataSource/Code.pm version: '0.44' UR::DataSource::Default: file: lib/UR/DataSource/Default.pm version: '0.44' UR::DataSource::File: file: lib/UR/DataSource/File.pm version: '0.44' UR::DataSource::FileMux: file: lib/UR/DataSource/FileMux.pm version: '0.44' UR::DataSource::Filesystem: file: lib/UR/DataSource/Filesystem.pm version: '0.44' UR::DataSource::Meta: file: lib/UR/DataSource/Meta.pm version: '0.44' UR::DataSource::MySQL: file: lib/UR/DataSource/MySQL.pm version: '0.44' UR::DataSource::Oracle: file: lib/UR/DataSource/Oracle.pm version: '0.44' UR::DataSource::Pg: file: lib/UR/DataSource/Pg.pm version: '0.44' UR::DataSource::Pg::Operator::False: file: lib/UR/DataSource/Pg/Operator/False.pm UR::DataSource::Pg::Operator::True: file: lib/UR/DataSource/Pg/Operator/True.pm UR::DataSource::QueryPlan: file: lib/UR/DataSource/QueryPlan.pm version: '0.44' UR::DataSource::RDBMS: file: lib/UR/DataSource/RDBMS.pm version: '0.44' UR::DataSource::RDBMS::BitmapIndex: file: lib/UR/DataSource/RDBMS/BitmapIndex.pm version: '0.44' UR::DataSource::RDBMS::Entity: file: lib/UR/DataSource/RDBMS/Entity.pm version: '0.44' UR::DataSource::RDBMS::FkConstraint: file: lib/UR/DataSource/RDBMS/FkConstraint.pm version: '0.44' UR::DataSource::RDBMS::FkConstraintColumn: file: lib/UR/DataSource/RDBMS/FkConstraintColumn.pm version: '0.44' UR::DataSource::RDBMS::Operator::Between: file: lib/UR/DataSource/RDBMS/Operator/Between.pm UR::DataSource::RDBMS::Operator::Equals: file: lib/UR/DataSource/RDBMS/Operator/Equals.pm UR::DataSource::RDBMS::Operator::False: file: lib/UR/DataSource/RDBMS/Operator/False.pm UR::DataSource::RDBMS::Operator::GreaterOrEqual: file: lib/UR/DataSource/RDBMS/Operator/GreaterOrEqual.pm UR::DataSource::RDBMS::Operator::GreaterThan: file: lib/UR/DataSource/RDBMS/Operator/GreaterThan.pm UR::DataSource::RDBMS::Operator::In: file: lib/UR/DataSource/RDBMS/Operator/In.pm UR::DataSource::RDBMS::Operator::LessOrEqual: file: lib/UR/DataSource/RDBMS/Operator/LessOrEqual.pm UR::DataSource::RDBMS::Operator::LessThan: file: lib/UR/DataSource/RDBMS/Operator/LessThan.pm UR::DataSource::RDBMS::Operator::Like: file: lib/UR/DataSource/RDBMS/Operator/Like.pm UR::DataSource::RDBMS::Operator::NotBetween: file: lib/UR/DataSource/RDBMS/Operator/NotBetween.pm UR::DataSource::RDBMS::Operator::NotEquals: file: lib/UR/DataSource/RDBMS/Operator/NotEquals.pm UR::DataSource::RDBMS::Operator::NotIn: file: lib/UR/DataSource/RDBMS/Operator/NotIn.pm UR::DataSource::RDBMS::Operator::NotLike: file: lib/UR/DataSource/RDBMS/Operator/NotLike.pm UR::DataSource::RDBMS::Operator::True: file: lib/UR/DataSource/RDBMS/Operator/True.pm UR::DataSource::RDBMS::PkConstraintColumn: file: lib/UR/DataSource/RDBMS/PkConstraintColumn.pm version: '0.44' UR::DataSource::RDBMS::Table: file: lib/UR/DataSource/RDBMS/Table.pm version: '0.44' UR::DataSource::RDBMS::Table::View::Default::Text: file: lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm version: '0.44' UR::DataSource::RDBMS::TableColumn: file: lib/UR/DataSource/RDBMS/TableColumn.pm version: '0.44' UR::DataSource::RDBMS::TableColumn::View::Default::Text: file: lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm version: '0.44' UR::DataSource::RDBMS::UniqueConstraintColumn: file: lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm version: '0.44' UR::DataSource::RDBMSRetriableOperations: file: lib/UR/DataSource/RDBMSRetriableOperations.pm UR::DataSource::SQLite: file: lib/UR/DataSource/SQLite.pm version: '0.44' UR::DataSource::ValueDomain: file: lib/UR/DataSource/ValueDomain.pm version: '0.44' UR::Debug: file: lib/UR/Debug.pm version: '0.44' UR::DeletedRef: file: lib/UR/DeletedRef.pm version: '0.44' UR::Doc::Pod2Html: file: lib/UR/Doc/Pod2Html.pm version: '0.44' UR::Doc::Section: file: lib/UR/Doc/Section.pm version: '0.44' UR::Doc::Writer: file: lib/UR/Doc/Writer.pm version: '0.44' UR::Doc::Writer::Html: file: lib/UR/Doc/Writer/Html.pm version: '0.44' UR::Doc::Writer::Pod: file: lib/UR/Doc/Writer/Pod.pm version: '0.44' UR::Env::UR_COMMAND_DUMP_DEBUG_MESSAGES: file: lib/UR/Env/UR_COMMAND_DUMP_DEBUG_MESSAGES.pm version: '0.44' UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES: file: lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm version: '0.44' UR::Env::UR_CONTEXT_BASE: file: lib/UR/Env/UR_CONTEXT_BASE.pm version: '0.44' UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER: file: lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm version: '0.44' UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER: file: lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm version: '0.44' UR::Env::UR_CONTEXT_LIBS: file: lib/UR/Env/UR_USED_LIBS.pm version: '0.44' UR::Env::UR_CONTEXT_MONITOR_QUERY: file: lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm version: '0.44' UR::Env::UR_CONTEXT_ROOT: file: lib/UR/Env/UR_CONTEXT_ROOT.pm version: '0.44' UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT: file: lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm version: '0.44' UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm version: '0.44' UR::Env::UR_DBI_EXPLAIN_SQL_IF: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm version: '0.44' UR::Env::UR_DBI_EXPLAIN_SQL_MATCH: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm version: '0.44' UR::Env::UR_DBI_EXPLAIN_SQL_SLOW: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm version: '0.44' UR::Env::UR_DBI_MONITOR_DML: file: lib/UR/Env/UR_DBI_MONITOR_DML.pm version: '0.44' UR::Env::UR_DBI_MONITOR_EVERY_FETCH: file: lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm version: '0.44' UR::Env::UR_DBI_MONITOR_SQL: file: lib/UR/Env/UR_DBI_MONITOR_SQL.pm version: '0.44' UR::Env::UR_DBI_NO_COMMIT: file: lib/UR/Env/UR_DBI_NO_COMMIT.pm version: '0.44' UR::Env::UR_DBI_SUMMARIZE_SQL: file: lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm version: '0.44' UR::Env::UR_DEBUG_OBJECT_PRUNING: file: lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm version: '0.44' UR::Env::UR_DEBUG_OBJECT_RELEASE: file: lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm version: '0.44' UR::Env::UR_DUMP_DEBUG_MESSAGES: file: lib/UR/Env/UR_DUMP_DEBUG_MESSAGES.pm version: '0.44' UR::Env::UR_DUMP_STATUS_MESSAGES: file: lib/UR/Env/UR_DUMP_STATUS_MESSAGES.pm version: '0.44' UR::Env::UR_IGNORE: file: lib/UR/Env/UR_IGNORE.pm version: '0.44' UR::Env::UR_NO_REQUIRE_USER_VERIFY: file: lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm version: '0.44' UR::Env::UR_NR_CPU: file: lib/UR/Env/UR_NR_CPU.pm version: '0.44' UR::Env::UR_RUN_LONG_TESTS: file: lib/UR/Env/UR_RUN_LONG_TESTS.pm version: '0.44' UR::Env::UR_STACK_DUMP_ON_DIE: file: lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm version: '0.44' UR::Env::UR_STACK_DUMP_ON_WARN: file: lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm version: '0.44' UR::Env::UR_TEST_QUIET: file: lib/UR/Env/UR_TEST_QUIET.pm version: '0.44' UR::Env::UR_USED_MODS: file: lib/UR/Env/UR_USED_MODS.pm version: '0.44' UR::Env::UR_USE_ANY: file: lib/UR/Env/UR_USE_ANY.pm version: '0.44' UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS: file: lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm version: '0.44' UR::Exit: file: lib/UR/Exit.pm version: '0.44' UR::ModuleBase: file: lib/UR/ModuleBase.pm version: '0.44' UR::ModuleBase::Message: file: lib/UR/ObjectDeprecated.pm UR::ModuleBuild: file: lib/UR/ModuleBuild.pm UR::ModuleConfig: file: lib/UR/ModuleConfig.pm version: '0.44' UR::ModuleLoader: file: lib/UR/ModuleLoader.pm version: '0.44' UR::Namespace: file: lib/UR/Namespace.pm version: '0.44' UR::Namespace::Command: file: lib/UR/Namespace/Command.pm version: '0.44' UR::Namespace::Command::Base: file: lib/UR/Namespace/Command/Base.pm version: '0.44' UR::Namespace::Command::Define: file: lib/UR/Namespace/Command/Define.pm version: '0.44' UR::Namespace::Command::Define::Class: file: lib/UR/Namespace/Command/Define/Class.pm version: '0.44' UR::Namespace::Command::Define::Datasource: file: lib/UR/Namespace/Command/Define/Datasource.pm version: '0.44' UR::Namespace::Command::Define::Datasource::File: file: lib/UR/Namespace/Command/Define/Datasource/File.pm version: '0.44' UR::Namespace::Command::Define::Datasource::Mysql: file: lib/UR/Namespace/Command/Define/Datasource/Mysql.pm version: '0.44' UR::Namespace::Command::Define::Datasource::Oracle: file: lib/UR/Namespace/Command/Define/Datasource/Oracle.pm version: '0.44' UR::Namespace::Command::Define::Datasource::Pg: file: lib/UR/Namespace/Command/Define/Datasource/Pg.pm version: '0.44' UR::Namespace::Command::Define::Datasource::Rdbms: file: lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm version: '0.44' UR::Namespace::Command::Define::Datasource::RdbmsWithAuth: file: lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm version: '0.44' UR::Namespace::Command::Define::Datasource::Sqlite: file: lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm version: '0.44' UR::Namespace::Command::Define::Db: file: lib/UR/Namespace/Command/Define/Db.pm version: '0.44' UR::Namespace::Command::Define::Namespace: file: lib/UR/Namespace/Command/Define/Namespace.pm version: '0.44' UR::Namespace::Command::Init: file: lib/UR/Namespace/Command/Init.pm version: '0.44' UR::Namespace::Command::List: file: lib/UR/Namespace/Command/List.pm version: '0.44' UR::Namespace::Command::List::Classes: file: lib/UR/Namespace/Command/List/Classes.pm version: '0.44' UR::Namespace::Command::List::Modules: file: lib/UR/Namespace/Command/List/Modules.pm version: '0.44' UR::Namespace::Command::List::Objects: file: lib/UR/Namespace/Command/List/Objects.pm version: '0.44' UR::Namespace::Command::Old: file: lib/UR/Namespace/Command/Old.pm version: '0.44' UR::Namespace::Command::Old::DiffRewrite: file: lib/UR/Namespace/Command/Old/DiffRewrite.pm version: '0.44' UR::Namespace::Command::Old::DiffUpdate: file: lib/UR/Namespace/Command/Old/DiffUpdate.pm version: '0.44' UR::Namespace::Command::Old::ExportDbicClasses: file: lib/UR/Namespace/Command/Old/ExportDbicClasses.pm version: '0.44' UR::Namespace::Command::Old::Info: file: lib/UR/Namespace/Command/Old/Info.pm version: '0.44' UR::Namespace::Command::Old::Redescribe: file: lib/UR/Namespace/Command/Old/Redescribe.pm version: '0.44' UR::Namespace::Command::RunsOnModulesInTree: file: lib/UR/Namespace/Command/RunsOnModulesInTree.pm version: '0.44' UR::Namespace::Command::Show: file: lib/UR/Namespace/Command/Show.pm UR::Namespace::Command::Show::Properties: file: lib/UR/Namespace/Command/Show/Properties.pm version: '0.44' UR::Namespace::Command::Show::Schema: file: lib/UR/Namespace/Command/Show/Schema.pm UR::Namespace::Command::Show::Subclasses: file: lib/UR/Namespace/Command/Show/Subclasses.pm UR::Namespace::Command::Sys: file: lib/UR/Namespace/Command/Sys.pm version: '0.44' UR::Namespace::Command::Sys::ClassBrowser: file: lib/UR/Namespace/Command/Sys/ClassBrowser.pm version: '0.44' UR::Namespace::Command::Sys::ClassBrowser::TreeItem: file: lib/UR/Namespace/Command/Sys/ClassBrowser.pm UR::Namespace::Command::Test: file: lib/UR/Namespace/Command/Test.pm version: '0.44' UR::Namespace::Command::Test::Callcount: file: lib/UR/Namespace/Command/Test/Callcount.pm version: '0.44' UR::Namespace::Command::Test::Callcount::List: file: lib/UR/Namespace/Command/Test/Callcount/List.pm version: '0.44' UR::Namespace::Command::Test::Compile: file: lib/UR/Namespace/Command/Test/Compile.pm version: '0.44' UR::Namespace::Command::Test::Eval: file: lib/UR/Namespace/Command/Test/Eval.pm version: '0.44' UR::Namespace::Command::Test::Run: file: lib/UR/Namespace/Command/Test/Run.pm version: '0.44' UR::Namespace::Command::Test::TrackObjectRelease: file: lib/UR/Namespace/Command/Test/TrackObjectRelease.pm version: '0.44' UR::Namespace::Command::Test::Use: file: lib/UR/Namespace/Command/Test/Use.pm version: '0.44' UR::Namespace::Command::Test::Window: file: lib/UR/Namespace/Command/Test/Window.pm version: '0.44' UR::Namespace::Command::Test::Window::Tk: file: lib/UR/Namespace/Command/Test/Window.pm UR::Namespace::Command::Update: file: lib/UR/Namespace/Command/Update.pm version: '0.44' UR::Namespace::Command::Update::ClassDiagram: file: lib/UR/Namespace/Command/Update/ClassDiagram.pm version: '0.44' UR::Namespace::Command::Update::ClassesFromDb: file: lib/UR/Namespace/Command/Update/ClassesFromDb.pm version: '0.44' UR::Namespace::Command::Update::Doc: file: lib/UR/Namespace/Command/Update/Doc.pm version: '0.44' UR::Namespace::Command::Update::Pod: file: lib/UR/Namespace/Command/Update/Pod.pm version: '0.44' UR::Namespace::Command::Update::RenameClass: file: lib/UR/Namespace/Command/Update/RenameClass.pm version: '0.44' UR::Namespace::Command::Update::RewriteClassHeader: file: lib/UR/Namespace/Command/Update/RewriteClassHeader.pm version: '0.44' UR::Namespace::Command::Update::SchemaDiagram: file: lib/UR/Namespace/Command/Update/SchemaDiagram.pm version: '0.44' UR::Namespace::Command::Update::TabCompletionSpec: file: lib/UR/Namespace/Command/Update/TabCompletionSpec.pm version: '0.44' UR::Object: file: lib/UR/Object.pm version: '0.44' UR::Object::Accessorized: file: lib/UR/Object/Accessorized.pm version: '0.44' UR::Object::Command::FetchAndDo: file: lib/UR/Object/Command/FetchAndDo.pm version: '0.44' UR::Object::Command::List: file: lib/UR/Object/Command/List.pm version: '0.44' UR::Object::Command::List::Csv: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Html: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Newtext: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Pretty: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Style: file: lib/UR/Object/Command/List/Style.pm version: '0.44' UR::Object::Command::List::Text: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Tsv: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Xml: file: lib/UR/Object/Command/List/Style.pm UR::Object::Ghost: file: lib/UR/Object/Ghost.pm version: '0.44' UR::Object::Index: file: lib/UR/Object/Index.pm version: '0.44' UR::Object::Iterator: file: lib/UR/Object/Iterator.pm version: '0.44' UR::Object::Join: file: lib/UR/Object/Join.pm version: '0.44' UR::Object::Property: file: lib/UR/Object/Property.pm version: '0.44' UR::Object::Property::View::Default::Text: file: lib/UR/Object/Property/View/Default/Text.pm version: '0.44' UR::Object::Property::View::DescriptionLineItem::Text: file: lib/UR/Object/Property/View/DescriptionLineItem/Text.pm version: '0.44' UR::Object::Property::View::ReferenceDescription::Text: file: lib/UR/Object/Property/View/ReferenceDescription/Text.pm version: '0.44' UR::Object::Set: file: lib/UR/Object/Set.pm version: '0.44' UR::Object::Set::View::Default::Html: file: lib/UR/Object/Set/View/Default/Html.pm version: '0.44' UR::Object::Set::View::Default::Json: file: lib/UR/Object/Set/View/Default/Json.pm version: '0.44' UR::Object::Set::View::Default::Text: file: lib/UR/Object/Set/View/Default/Text.pm version: '0.44' UR::Object::Set::View::Default::Xml: file: lib/UR/Object/Set/View/Default/Xml.pm version: '0.44' UR::Object::Tag: file: lib/UR/Object/Tag.pm version: '0.44' UR::Object::Type: file: lib/UR/Object/Type.pm version: '0.44' UR::Object::Type::AccessorWriter: file: lib/UR/Object/Type/AccessorWriter.pm UR::Object::Type::AccessorWriter::Product: file: lib/UR/Object/Type/AccessorWriter/Product.pm version: '0.44' UR::Object::Type::AccessorWriter::Sum: file: lib/UR/Object/Type/AccessorWriter/Sum.pm version: '0.44' UR::Object::Type::Initializer: file: lib/UR/Object/Type/Initializer.pm UR::Object::Type::ModuleWriter: file: lib/UR/Object/Type/ModuleWriter.pm UR::Object::Type::View::AvailableViews::Json: file: lib/UR/Object/Type/View/AvailableViews/Json.pm version: '0.44' UR::Object::Type::View::AvailableViews::Xml: file: lib/UR/Object/Type/View/AvailableViews/Xml.pm version: '0.44' UR::Object::Type::View::Default::Text: file: lib/UR/Object/Type/View/Default/Text.pm version: '0.44' UR::Object::Type::View::Default::Xml: file: lib/UR/Object/Type/View/Default/Xml.pm version: '0.44' UR::Object::Value: file: lib/UR/Object/Value.pm version: '0.44' UR::Object::View: file: lib/UR/Object/View.pm version: '0.44' UR::Object::View::Aspect: file: lib/UR/Object/View/Aspect.pm version: '0.44' UR::Object::View::Default::Gtk: file: lib/UR/Object/View/Default/Gtk.pm version: '0.44' UR::Object::View::Default::Gtk2: file: lib/UR/Object/View/Default/Gtk2.pm version: '0.44' UR::Object::View::Default::Html: file: lib/UR/Object/View/Default/Html.pm version: '0.44' UR::Object::View::Default::Json: file: lib/UR/Object/View/Default/Json.pm version: '0.44' UR::Object::View::Default::Text: file: lib/UR/Object/View/Default/Text.pm version: '0.44' UR::Object::View::Default::Xml: file: lib/UR/Object/View/Default/Xml.pm version: '0.44' UR::Object::View::Default::Xsl: file: lib/UR/Object/View/Default/Xsl.pm version: '0.44' UR::Object::View::Lister::Text: file: lib/UR/Object/View/Lister/Text.pm version: '0.44' UR::Object::View::Static::Html: file: lib/UR/Object/View/Static/Html.pm version: '0.44' UR::Object::View::Toolkit: file: lib/UR/Object/View/Toolkit.pm version: '0.44' UR::Object::View::Toolkit::Text: file: lib/UR/Object/View/Toolkit/Text.pm version: '0.44' UR::Observer: file: lib/UR/Observer.pm version: '0.44' UR::Service::JsonRpcServer: file: lib/UR/Service/JsonRpcServer.pm version: '0.44' UR::Service::RPC::Executer: file: lib/UR/Service/RPC/Executer.pm version: '0.44' UR::Service::RPC::Message: file: lib/UR/Service/RPC/Message.pm version: '0.44' UR::Service::RPC::Server: file: lib/UR/Service/RPC/Server.pm version: '0.44' UR::Service::RPC::TcpConnectionListener: file: lib/UR/Service/RPC/TcpConnectionListener.pm version: '0.44' UR::Service::UrlRouter: file: lib/UR/Service/UrlRouter.pm UR::Service::WebServer: file: lib/UR/Service/WebServer.pm UR::Service::WebServer::Server: file: lib/UR/Service/WebServer/Server.pm UR::Singleton: file: lib/UR/Singleton.pm version: '0.44' UR::Test: file: lib/UR/Test.pm UR::Util: file: lib/UR/Util.pm version: '0.44' UR::Util::ArrayRefIterator: file: lib/UR/Util/ArrayRefIterator.pm UR::Value: file: lib/UR/Value.pm version: '0.44' UR::Value::ARRAY: file: lib/UR/Value/ARRAY.pm version: '0.44' UR::Value::Blob: file: lib/UR/Value/Blob.pm version: '0.44' UR::Value::Boolean: file: lib/UR/Value/Boolean.pm version: '0.44' UR::Value::Boolean::View::Default::Text: file: lib/UR/Value/Boolean/View/Default/Text.pm version: '0.44' UR::Value::CODE: file: lib/UR/Value/CODE.pm version: '0.44' UR::Value::CSV: file: lib/UR/Value/CSV.pm version: '0.44' UR::Value::DateTime: file: lib/UR/Value/DateTime.pm version: '0.44' UR::Value::Decimal: file: lib/UR/Value/Decimal.pm version: '0.44' UR::Value::DirectoryPath: file: lib/UR/Value/DirectoryPath.pm version: '0.44' UR::Value::FOF: file: lib/UR/Value/FOF.pm version: '0.44' UR::Value::FilePath: file: lib/UR/Value/FilePath.pm version: '0.44' UR::Value::FilesystemPath: file: lib/UR/Value/FilesystemPath.pm version: '0.44' UR::Value::Float: file: lib/UR/Value/Float.pm version: '0.44' UR::Value::GLOB: file: lib/UR/Value/GLOB.pm version: '0.44' UR::Value::HASH: file: lib/UR/Value/HASH.pm version: '0.44' UR::Value::Integer: file: lib/UR/Value/Integer.pm version: '0.44' UR::Value::Iterator: file: lib/UR/Value/Iterator.pm version: '0.44' UR::Value::JSON: file: lib/UR/Value/JSON.pm UR::Value::Number: file: lib/UR/Value/Number.pm version: '0.44' UR::Value::PerlReference: file: lib/UR/Value/PerlReference.pm version: '0.44' UR::Value::REF: file: lib/UR/Value/REF.pm version: '0.44' UR::Value::SCALAR: file: lib/UR/Value/SCALAR.pm version: '0.44' UR::Value::Set: file: lib/UR/Value/Set.pm version: '0.44' UR::Value::SloppyPrimitive: file: lib/UR/Value/SloppyPrimitive.pm version: '0.44' UR::Value::String: file: lib/UR/Value/String.pm version: '0.44' UR::Value::Text: file: lib/UR/Value/Text.pm version: '0.44' UR::Value::Timestamp: file: lib/UR/Value/Timestamp.pm version: '0.44' UR::Value::Type: file: lib/UR/Value.pm UR::Value::URL: file: lib/UR/Value/URL.pm version: '0.44' UR::Value::View::Default::Html: file: lib/UR/Value/View/Default/Html.pm UR::Value::View::Default::Json: file: lib/UR/Value/View/Default/Json.pm UR::Value::View::Default::Text: file: lib/UR/Value/View/Default/Text.pm UR::Value::View::Default::Xml: file: lib/UR/Value/View/Default/Xml.pm UR::Vocabulary: file: lib/UR/Vocabulary.pm version: '0.44' above: file: lib/above.pm version: '0.03' class_name: file: lib/UR/Object/Type/Initializer.pm version: '2' requires: Carp: '0' Class::AutoloadCAN: '0.03' Class::Autouse: '2.0' Clone::PP: '1.02' DBD::SQLite: '1.14' DBI: '1.601' Data::Compare: '0.13' Data::UUID: '0.148' Date::Format: '0' Devel::GlobalDestruction: '0' File::Basename: '2.73' File::Path: '0' File::Temp: '0' FreezeThaw: '0.43' Getopt::Complete: '0.26' HTTP::Request: '0' JSON: '0' Lingua::EN::Inflect: '1.88' List::MoreUtils: '0' MRO::Compat: '0' Module::Runtime: v0.14.0 Path::Class: '0' Plack: '0' Pod::Simple::HTML: '3.03' Pod::Simple::Text: '2.02' Sub::Install: '0.924' Sub::Name: '0.04' Sys::Hostname: '1.11' Template: '0' Text::Diff: '0.35' Text::Glob: '0' YAML: '0' perl: v5.8.7 version: '0' resources: bugtracker: https://github.com/genome/UR/issues homepage: https://github.com/genome/UR repository: git://github.com/genome/UR.git version: '0.44' x_contributors: - 'josborne ' - 'thepler ' - 'charris ' - 'jweible ' - 'edemello ' - 'jschindl ' - 'mjohnson ' - 'ebelter ' - 'lcarmich ' - 'jwalker ' - 'gsanders ' - 'eclark ' - 'adukes ' - 'iferguso ' - 'ssmith ' - 'boberkfe ' - 'Mark Johnson ' - 'Ben Oberkfell ' - 'ssmith ' - 'Edward Belter ' - 'Feiyu Du ' - 'Edward Belter ' - 'Feiyu Du ' - 'ssmith ' - 'ssmith ' - 'Robert Long ' - 'Robert Long ' - 'Thomas Mooney ' - 'Eric Clark ' - 'Thomas Mooney ' - 'ssmith ' - 'Scott Smith X ' - 'eclark ' - 'Anthony Brummett ' - 'Rob Long ' - 'Adam Dukes ' - 'Eddie Belter ' - 'Kyung Kim ' - 'Joshua McMichael ' - 'Jim Weible ' - 'Philip Kimmey ' - 'Matt Callaway ' - 'Travis Abbott ' - 'Chris Oliver ' - 'Steven Wallace ' - 'James Koval ' - 'Gabriel Sanderson ' - 'Eddie Belter ' - 'Scott Smith ' - 'Brian Derickson ' - 'Justin Lolofie ' - 'Ian Ferguson ' - 'Neil Bowers ' - 'Scott Smith ' - 'Ben Oberkfell ' - 'Jason Walker ' - 'Mark Burnett ' - 'Scott Smith ' - 'Adam Coffman ' - 'Susanna Siebert ' - 'apregier ' - 'David Morton ' - 'David Morton ' - 'Sebastian Heil <(none)>' - 'APipe Tester ' - 'nnutter ' - 'Nathaniel Nutter ' - 'Thomas B. Mooney ' - 'Anthony Brummett ' - 'Anthony Brummett ' MANIFEST100664023532023421 6377712544604517 13033 0ustar00abrummetgsc000000000000UR-0.44Build.PL Changes INSTALL LICENSE META.json README.md bin/ur builder/UR.pm cpanfile dist-maint/README dist-maint/findreplace dist-maint/update-pod.sh dist-maint/update-ur-all.sh dist-maint/update-version.pl gmt-web/common.yml gmt-web/content/documentation.html gmt-web/content/index.html gmt-web/content/install.md gmt-web/content/res/images/icon_16.png gmt-web/content/res/images/icon_48.png lib/Command.pm lib/Command/Common.pm lib/Command/Dispatch/Shell.pm lib/Command/DynamicSubCommands.pm lib/Command/Shell.pm lib/Command/SubCommandFactory.pm lib/Command/Test.pm lib/Command/Test/Echo.pm lib/Command/Test/Tree1.pm lib/Command/Test/Tree1/Echo1.pm lib/Command/Test/Tree1/Echo2.pm lib/Command/Tree.pm lib/Command/V1.pm lib/Command/V2.pm lib/Command/V2Deprecated.pm lib/Command/View/DocMethods.pm lib/Devel/callcount.pm lib/UR.pm lib/UR/All.pm lib/UR/BoolExpr.pm lib/UR/BoolExpr/BxParser.pm lib/UR/BoolExpr/BxParser.yp lib/UR/BoolExpr/Template.pm lib/UR/BoolExpr/Template/And.pm lib/UR/BoolExpr/Template/Composite.pm lib/UR/BoolExpr/Template/Or.pm lib/UR/BoolExpr/Template/PropertyComparison.pm lib/UR/BoolExpr/Template/PropertyComparison/Between.pm lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm lib/UR/BoolExpr/Template/PropertyComparison/False.pm lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm lib/UR/BoolExpr/Template/PropertyComparison/In.pm lib/UR/BoolExpr/Template/PropertyComparison/Isa.pm lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm lib/UR/BoolExpr/Template/PropertyComparison/Like.pm lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm lib/UR/BoolExpr/Template/PropertyComparison/NotBetween.pm lib/UR/BoolExpr/Template/PropertyComparison/NotEquals.pm lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm lib/UR/BoolExpr/Template/PropertyComparison/True.pm lib/UR/BoolExpr/Util.pm lib/UR/Change.pm lib/UR/Context.pm lib/UR/Context/AutoUnloadPool.pm lib/UR/Context/DefaultRoot.pm lib/UR/Context/ImportIterator.pm lib/UR/Context/LoadingIterator.pm lib/UR/Context/ObjectFabricator.pm lib/UR/Context/Process.pm lib/UR/Context/Root.pm lib/UR/Context/Transaction.pm lib/UR/DBI.pm lib/UR/DBI/Report.pm lib/UR/DataSource.pm lib/UR/DataSource.pod lib/UR/DataSource/CSV.pm lib/UR/DataSource/Code.db lib/UR/DataSource/Code.pm lib/UR/DataSource/Code.schema lib/UR/DataSource/Default.pm lib/UR/DataSource/File.pm lib/UR/DataSource/FileMux.pm lib/UR/DataSource/Filesystem.pm lib/UR/DataSource/Meta.pm lib/UR/DataSource/Meta.sqlite3-dump lib/UR/DataSource/Meta.sqlite3-dump-boostrap lib/UR/DataSource/Meta.sqlite3-schema lib/UR/DataSource/MySQL.pm lib/UR/DataSource/Oracle.pm lib/UR/DataSource/Pg.pm lib/UR/DataSource/Pg/Operator/False.pm lib/UR/DataSource/Pg/Operator/True.pm lib/UR/DataSource/QueryPlan.pm lib/UR/DataSource/RDBMS.pm lib/UR/DataSource/RDBMS/BitmapIndex.pm lib/UR/DataSource/RDBMS/Entity.pm lib/UR/DataSource/RDBMS/FkConstraint.pm lib/UR/DataSource/RDBMS/FkConstraintColumn.pm lib/UR/DataSource/RDBMS/Operator/Between.pm lib/UR/DataSource/RDBMS/Operator/Equals.pm lib/UR/DataSource/RDBMS/Operator/False.pm lib/UR/DataSource/RDBMS/Operator/GreaterOrEqual.pm lib/UR/DataSource/RDBMS/Operator/GreaterThan.pm lib/UR/DataSource/RDBMS/Operator/In.pm lib/UR/DataSource/RDBMS/Operator/LessOrEqual.pm lib/UR/DataSource/RDBMS/Operator/LessThan.pm lib/UR/DataSource/RDBMS/Operator/Like.pm lib/UR/DataSource/RDBMS/Operator/NotBetween.pm lib/UR/DataSource/RDBMS/Operator/NotEquals.pm lib/UR/DataSource/RDBMS/Operator/NotIn.pm lib/UR/DataSource/RDBMS/Operator/NotLike.pm lib/UR/DataSource/RDBMS/Operator/True.pm lib/UR/DataSource/RDBMS/PkConstraintColumn.pm lib/UR/DataSource/RDBMS/Table.pm lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm lib/UR/DataSource/RDBMS/TableColumn.pm lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm lib/UR/DataSource/RDBMSRetriableOperations.pm lib/UR/DataSource/SQLite.pm lib/UR/DataSource/ValueDomain.pm lib/UR/Debug.pm lib/UR/DeletedRef.pm lib/UR/Doc/Pod2Html.pm lib/UR/Doc/Section.pm lib/UR/Doc/Writer.pm lib/UR/Doc/Writer/Html.pm lib/UR/Doc/Writer/Pod.pm lib/UR/Env.pod lib/UR/Env/UR_COMMAND_DUMP_DEBUG_MESSAGES.pm lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm lib/UR/Env/UR_CONTEXT_BASE.pm lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm lib/UR/Env/UR_CONTEXT_ROOT.pm lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm lib/UR/Env/UR_DBI_MONITOR_DML.pm lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm lib/UR/Env/UR_DBI_MONITOR_SQL.pm lib/UR/Env/UR_DBI_NO_COMMIT.pm lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm lib/UR/Env/UR_DUMP_DEBUG_MESSAGES.pm lib/UR/Env/UR_DUMP_STATUS_MESSAGES.pm lib/UR/Env/UR_IGNORE.pm lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm lib/UR/Env/UR_NR_CPU.pm lib/UR/Env/UR_RUN_LONG_TESTS.pm lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm lib/UR/Env/UR_TEST_QUIET.pm lib/UR/Env/UR_USED_LIBS.pm lib/UR/Env/UR_USED_MODS.pm lib/UR/Env/UR_USE_ANY.pm lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm lib/UR/Exit.pm lib/UR/Manual.pod lib/UR/Manual/Cookbook.pod lib/UR/Manual/Metadata.pod lib/UR/Manual/Overview.pod lib/UR/Manual/Presentation.pod lib/UR/Manual/SchemaDesign.pod lib/UR/Manual/Tutorial.pod lib/UR/Manual/UR_Presentation.pdf lib/UR/ModuleBase.pm lib/UR/ModuleBuild.pm lib/UR/ModuleConfig.pm lib/UR/ModuleLoader.pm lib/UR/Namespace.pm lib/UR/Namespace/Command.pm lib/UR/Namespace/Command.pm.opts lib/UR/Namespace/Command/Base.pm lib/UR/Namespace/Command/Define.pm lib/UR/Namespace/Command/Define/Class.pm lib/UR/Namespace/Command/Define/Datasource.pm lib/UR/Namespace/Command/Define/Datasource/File.pm lib/UR/Namespace/Command/Define/Datasource/Mysql.pm lib/UR/Namespace/Command/Define/Datasource/Oracle.pm lib/UR/Namespace/Command/Define/Datasource/Pg.pm lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm lib/UR/Namespace/Command/Define/Db.pm lib/UR/Namespace/Command/Define/Namespace.pm lib/UR/Namespace/Command/Init.pm lib/UR/Namespace/Command/List.pm lib/UR/Namespace/Command/List/Classes.pm lib/UR/Namespace/Command/List/Modules.pm lib/UR/Namespace/Command/List/Objects.pm lib/UR/Namespace/Command/Old.pm lib/UR/Namespace/Command/Old/DiffRewrite.pm lib/UR/Namespace/Command/Old/DiffUpdate.pm lib/UR/Namespace/Command/Old/ExportDbicClasses.pm lib/UR/Namespace/Command/Old/Info.pm lib/UR/Namespace/Command/Old/Redescribe.pm lib/UR/Namespace/Command/RunsOnModulesInTree.pm lib/UR/Namespace/Command/Show.pm lib/UR/Namespace/Command/Show/Properties.pm lib/UR/Namespace/Command/Show/Schema.pm lib/UR/Namespace/Command/Show/Subclasses.pm lib/UR/Namespace/Command/Sys.pm lib/UR/Namespace/Command/Sys/ClassBrowser.pm lib/UR/Namespace/Command/Sys/ClassBrowser/assets/css/bootstrap-responsive.min.css lib/UR/Namespace/Command/Sys/ClassBrowser/assets/css/bootstrap.min.css lib/UR/Namespace/Command/Sys/ClassBrowser/assets/css/class-browser.css lib/UR/Namespace/Command/Sys/ClassBrowser/assets/css/treeview-icons.png lib/UR/Namespace/Command/Sys/ClassBrowser/assets/img/glyphicons-halflings-white.png lib/UR/Namespace/Command/Sys/ClassBrowser/assets/img/glyphicons-halflings.png lib/UR/Namespace/Command/Sys/ClassBrowser/assets/js/bootstrap.min.js lib/UR/Namespace/Command/Sys/ClassBrowser/assets/js/class-browser.js lib/UR/Namespace/Command/Sys/ClassBrowser/assets/js/jquery.min.js lib/UR/Namespace/Command/Sys/ClassBrowser/assets/js/table-sorter.js lib/UR/Namespace/Command/Sys/ClassBrowser/class-browser.html lib/UR/Namespace/Command/Sys/ClassBrowser/class-detail.html lib/UR/Namespace/Command/Sys/ClassBrowser/partials/class_inheritance_tree.html lib/UR/Namespace/Command/Sys/ClassBrowser/partials/class_method_table.html lib/UR/Namespace/Command/Sys/ClassBrowser/partials/path_treeview.html lib/UR/Namespace/Command/Sys/ClassBrowser/partials/property_metadata_list.html lib/UR/Namespace/Command/Sys/ClassBrowser/render-perl-module.html lib/UR/Namespace/Command/Sys/ClassBrowser/search_results.html lib/UR/Namespace/Command/Test.pm lib/UR/Namespace/Command/Test/Callcount.pm lib/UR/Namespace/Command/Test/Callcount/List.pm lib/UR/Namespace/Command/Test/Compile.pm lib/UR/Namespace/Command/Test/Eval.pm lib/UR/Namespace/Command/Test/Run.pm lib/UR/Namespace/Command/Test/TrackObjectRelease.pm lib/UR/Namespace/Command/Test/Use.pm lib/UR/Namespace/Command/Test/Window.pm lib/UR/Namespace/Command/Update.pm lib/UR/Namespace/Command/Update/ClassDiagram.pm lib/UR/Namespace/Command/Update/ClassesFromDb.pm lib/UR/Namespace/Command/Update/Doc.pm lib/UR/Namespace/Command/Update/Pod.pm lib/UR/Namespace/Command/Update/RenameClass.pm lib/UR/Namespace/Command/Update/RewriteClassHeader.pm lib/UR/Namespace/Command/Update/SchemaDiagram.pm lib/UR/Namespace/Command/Update/TabCompletionSpec.pm lib/UR/Object.pm lib/UR/Object/Accessorized.pm lib/UR/Object/Command/FetchAndDo.pm lib/UR/Object/Command/List.pm lib/UR/Object/Command/List.pod lib/UR/Object/Command/List/Style.pm lib/UR/Object/Ghost.pm lib/UR/Object/Index.pm lib/UR/Object/Iterator.pm lib/UR/Object/Join.pm lib/UR/Object/Property.pm lib/UR/Object/Property/View/Default/Text.pm lib/UR/Object/Property/View/DescriptionLineItem/Text.pm lib/UR/Object/Property/View/ReferenceDescription/Text.pm lib/UR/Object/Set.pm lib/UR/Object/Set/View/Default/Html.pm lib/UR/Object/Set/View/Default/Json.pm lib/UR/Object/Set/View/Default/Text.pm lib/UR/Object/Set/View/Default/Xml.pm lib/UR/Object/Tag.pm lib/UR/Object/Type.pm lib/UR/Object/Type.pod lib/UR/Object/Type/AccessorWriter.pm lib/UR/Object/Type/AccessorWriter/Product.pm lib/UR/Object/Type/AccessorWriter/Sum.pm lib/UR/Object/Type/Initializer.pm lib/UR/Object/Type/Initializer.pod lib/UR/Object/Type/InternalAPI.pm lib/UR/Object/Type/ModuleWriter.pm lib/UR/Object/Type/View/AvailableViews/Json.pm lib/UR/Object/Type/View/AvailableViews/Xml.pm lib/UR/Object/Type/View/Default/Text.pm lib/UR/Object/Type/View/Default/Xml.pm lib/UR/Object/Value.pm lib/UR/Object/View.pm lib/UR/Object/View/Aspect.pm lib/UR/Object/View/Default/Gtk.pm lib/UR/Object/View/Default/Gtk2.pm lib/UR/Object/View/Default/Html.pm lib/UR/Object/View/Default/Json.pm lib/UR/Object/View/Default/Text.pm lib/UR/Object/View/Default/Xml.pm lib/UR/Object/View/Default/Xsl.pm lib/UR/Object/View/Lister/Text.pm lib/UR/Object/View/Static/Html.pm lib/UR/Object/V t/URT/t/04f_filemux.t t/URT/t/04f_filemux_sync_database.t t/URT/t/04g_rdbms_shared_table_name.t t/URT/t/04h_default_datasource.t t/URT/t/05_get_create_get.t t/URT/t/06_accessor_simple.t t/URT/t/07_create_get_simple.t t/URT/t/08_create_get_operators.t t/URT/t/10_accessor_object.t t/URT/t/11_create_with_delegated_property.t t/URT/t/11b_via_to_without_type.t t/URT/t/11c_create_with_via_property.t t/URT/t/11d_create_with_single_delegated_property_via_is_many_property.t t/URT/t/11e_copy.t t/URT/t/12_properties_metadata_query.t t/URT/t/13a_messaging.t t/URT/t/13b_dump_message_inheritance.t t/URT/t/13c_message_observers.t t/URT/t/13d_command_debug.t t/URT/t/13e_messaging_format_string.t t/URT/t/14_ghost_objects.t t/URT/t/15_singleton.t t/URT/t/16_viewer.t t/URT/t/17_accessor_object_basic.t t/URT/t/17b_mk_rw_accessor_signals_property_change.t t/URT/t/17c_rw_property_alias.t t/URT/t/18_indirect_accessor.t t/URT/t/19_calculated_accessor.t t/URT/t/20_has_many.t t/URT/t/20a_has_many_with_multiple_ids.t t/URT/t/21_observer.t t/URT/t/21b_load_observer_autosubclass.t t/URT/t/21c_load_observer_abstract_parent.t t/URT/t/21d_db_entity_observers.t t/URT/t/21e_old_subscription_api.t t/URT/t/21f_observer_priority.t t/URT/t/21g_subclass_loaded_observer.t t/URT/t/21h_multi_inherit_observer.t t/URT/t/21i_defaults.t t/URT/t/21j_register_callback.t t/URT/t/22_cached_get_with_subclasses.t t/URT/t/23_id_class_by_accessor.t t/URT/t/24_query_by_is_calculated.t t/URT/t/24_query_by_is_transient.t t/URT/t/24_query_via_method_call.t t/URT/t/25_recurse_get.t t/URT/t/26_indirect_mutator_with_where_via_is_many.t t/URT/t/27_get_with_limit_offset.t t/URT/t/28_dont_index_delegated_props.t t/URT/t/29_indirect_calculated_accessor.t t/URT/t/29b_join_calculated_accessor.t t/URT/t/29c_join_indirect_accessor.t t/URT/t/30_calculated_default.t t/URT/t/30_default_values.t t/URT/t/31_ref_as_value.t t/URT/t/32_ur_object_id.t t/URT/t/33_multiple_inheritance_for_same_table.t t/URT/t/34_autouse_with_circular_ur_classdef.t t/URT/t/35_all_objects_are_loaded_subclass.t t/URT/t/36_superclass_already_loaded.t t/URT/t/37_caching_with_in_clause.t t/URT/t/37b_caching_with_in_clause.t t/URT/t/38_join_across_data_sources.t t/URT/t/39_has_many.t t/URT/t/39b_has_many.t t/URT/t/39c_has_many.t t/URT/t/39c_singular_reverse_as.t t/URT/t/39d_composite_id_by.t t/URT/t/40_has_many_direct.t t/URT/t/41_rpc_basic.t t/URT/t/42_rpc_between_processes.t t/URT/t/43_infer_values_from_rule.t t/URT/t/44_modulewriter.t t/URT/t/45_deleted_subclassed_objects_stay_deleted.t t/URT/t/45_rollback_deleted_object.t t/URT/t/46_meta_property_relationships.t t/URT/t/47_indirect_is_many_accessor.t t/URT/t/47b_indirect_is_many_accessor_mutable_with_id_class_by.t t/URT/t/47c_is_many_accessor_with_id_class_by.t t/URT/t/48_inline_datasources.t t/URT/t/49_complicated_get.t t/URT/t/49b_complicated_get_2.t t/URT/t/49c_complicated_get_3.t t/URT/t/49d_complicated_get_joining_through_view.t t/URT/t/49e_complicated_get_joining_through_view2.t t/URT/t/49f_complicated_get_indirect_id_by.t t/URT/t/49g_complicated_get_double_join.t t/URT/t/49h_complicated_get_double_join.t t/URT/t/49i_complicated_get_join_through_value_class.t t/URT/t/49j_complicated_get_join_ends_at_value_class.t t/URT/t/49k_complicated_get_joins_with_hangoff_filter.t t/URT/t/49l_complicated_get_id_by_attribute.t t/URT/t/49m_reverse_as_is_delegated.t t/URT/t/49n_double_join_involves_inheritance.t t/URT/t/50_force_always_reload.t t/URT/t/50_get_and_reload.t t/URT/t/50_load_objects_that_stringify_false.t t/URT/t/50_unload_and_reload.t t/URT/t/50b_get_via_sql.t t/URT/t/51_get_with_hints.t t/URT/t/51b_unmatched_hints_query_cache.t t/URT/t/52_limit_cache_size.t t/URT/t/53_abandoned_iterator.t t/URT/t/54_valid_values.t t/URT/t/55_on_the_fly_metadb.t t/URT/t/55b_partial_metada_data.t t/URT/t/56_order_by_returns_items_in_order.t t/URT/t/56b_order_by_calculated_property.t t/URT/t/56c_via_property_with_order_by.t t/URT/t/57_order_by_merge_new_objects.t t/URT/t/58_order_by_merge_changed_objects.t t/URT/t/59_get_merge_new_objs_with_db.t t/URT/t/60_get_merge_changed_objs_with_db.t t/URT/t/60_sql_query_hint.t t/URT/t/61_iterator.t t/URT/t/61_iterator_merge_changed_objs_with_db.t t/URT/t/61a_iterator_with_or_boolexpr.t t/URT/t/62_in_not_in_operator.t t/URT/t/62b_in_not_in_operator.t t/URT/t/63_view_text.t t/URT/t/63b_view_with_subviews.t t/URT/t/63c_view_with_subviews.t t/URT/t/63c_view_with_subviews.t.expected.cat_set.json t/URT/t/63c_view_with_subviews.t.expected.cat_set.text t/URT/t/63c_view_with_subviews.t.expected.cat_set.xml t/URT/t/63c_view_with_subviews.t.expected.person.json t/URT/t/63c_view_with_subviews.t.expected.person.text t/URT/t/63c_view_with_subviews.t.expected.person.xml t/URT/t/63d_delete_view.t t/URT/t/63e_enumerate_available_views.t t/URT/t/64_nullable_foreign_key_handling_on_insert_and_delete.t t/URT/t/65_reload_with_changing_db_data.t t/URT/t/66_nullable_hangoff_data.t t/URT/t/67_composite_id_with_id_class_by_rt55121.t t/URT/t/68_trapped_death_does_not_stack_trace.t t/URT/t/69_subclassify_by.t t/URT/t/69_subclassify_by_db.t t/URT/t/70_command_arg_processing.t t/URT/t/70_command_help_text.t t/URT/t/70c_command_tree_usage_text.t t/URT/t/71_ur_value.t t/URT/t/71_ur_value_json.t t/URT/t/71_ur_value_multiple_id_properties.t t/URT/t/72_command_name_validation.t t/URT/t/73_opts_spec_creation_and_validation.t t/URT/t/74_xsl_view_url_convert.t t/URT/t/76_is_many_default_values.t t/URT/t/77_file_undef_value_handling.t t/URT/t/77_index_undef_value_handling.t t/URT/t/77_sql_undef_value_handling.t t/URT/t/78_get_by_subclass_params_load_properly.t t/URT/t/78b_get_by_subclass_property.t t/URT/t/79_like_operator.t t/URT/t/80_command_define_datasource.t t/URT/t/80b_namespace_command_base.t t/URT/t/80c_command_describe.t t/URT/t/80d_command_list.t t/URT/t/81_crud_custom_columnnames.t t/URT/t/82_boolexpr_op_underscore.t t/URT/t/82a_boolexpr_op_case_insensitive.t t/URT/t/83_commit_between_schemas.t t/URT/t/84_class_definition_errors.t t/URT/t/84b_implied_properties.t t/URT/t/85_avoid_loading_using_hints.t t/URT/t/85_method_meta.t t/URT/t/85b_avoid_loading_using_hints.t t/URT/t/86_custom_load.t t/URT/t/86b-custom-load-join.t t/URT/t/87_attributes_have.t t/URT/t/87_get_by_different_params_updates_query_cache.t t/URT/t/87_is_many_indirect_is_efficient.t t/URT/t/87a_many_to_many_query_is_efficient.t t/URT/t/87b_is_many_id_class_by_is_efficient.t t/URT/t/87c_query_by_is_many_indirect_is_efficient.t t/URT/t/87d_query_by_is_many_indirect_is_efficient.t t/URT/t/87e_missing_hangoff_data_is_efficient.t t/URT/t/87f_via_property_joins_to_itself.t t/URT/t/87g_doubly_delegated_multiple_pk_works.t t/URT/t/89_loading_with_boolexpr_evaluate.t t/URT/t/90_comparison_value_and_escape_character_to_regex.t t/URT/t/91_object_sets.t t/URT/t/91b_sets_count_with_changes.t t/URT/t/91c_set_relay.t t/URT/t/91d_basic_set.t t/URT/t/91e_via_set.t t/URT/t/92_copy_loaded_objects_to_alternate_db.t t/URT/t/92_save_object_with_propertyless_column.t t/URT/t/93_namespace.t t/URT/t/93b_namespace_loaded_from_symlink.t t/URT/t/94_chain_join.t t/URT/t/94b_flatten_reframe.t t/URT/t/95_detect_db_deleted.t t/URT/t/95_normalize_property_description.t t/URT/t/95b_subclass_description_preprocessor_errors.t t/URT/t/95c_detect_changed_in_memory_filter.t t/URT/t/96_context_clear_cache.t t/URT/t/96b_ur_context_class_commit_triggers_observer.t t/URT/t/96c_ur_context_current_and_process.t t/URT/t/97_used_libs.t t/URT/t/98_ur_update.t t/URT/t/99-autounload-pool.t t/URT/t/99-transaction-unload-defined-objects.t t/URT/t/99_transaction-failed_commit_rollback.t t/URT/t/99_transaction-observers.t t/URT/t/99_transaction.t t/URT/t/99_transaction_change_log_order.t t/URT/t/99_transaction_eval_or_do.t t/URT/t/99_transaction_log_all_changes.t t/URT/t/99_transaction_rollback_after_create.t t/URT/t/99_transaction_unload.t t/URT/t/file_datasource/path_spec_expansion.t t/URT/t/file_datasource/read.t t/URT/t/file_datasource/read_columns_from_header.t t/URT/t/file_datasource/read_efficiency.t t/URT/t/file_datasource/read_files_as_tables.t t/URT/t/file_datasource/read_linenum_as_column.t t/URT/t/file_datasource/read_multichar_record_sep.t t/URT/t/file_datasource/read_order_by.t t/URT/t/file_datasource/write.t t/URT/t/mro.t t/URT/t/resolve_param_value_from_cmdline_text.t t/URT/t/services/url-router.t t/URT/t/services/webserver.t t/URT/t/ur_data_type_for_data_source_data_type.t t/Vending.pm t/Vending/Coin.pm t/Vending/CoinType.pm t/Vending/Command.pm t/Vending/Command/Buy.pm t/Vending/Command/CoinReturn.pm t/Vending/Command/Dime.pm t/Vending/Command/Dollar.pm t/Vending/Command/InsertMoney.pm t/Vending/Command/Menu.pm t/Vending/Command/Nickel.pm t/Vending/Command/Outputter.pm t/Vending/Command/Quarter.pm t/Vending/Command/Service.pm t/Vending/Command/Service/Add.pm t/Vending/Command/Service/Add/Change.pm t/Vending/Command/Service/Add/Inventory.pm t/Vending/Command/Service/Add/Slot.pm t/Vending/Command/Service/ConfigureSlot.pm t/Vending/Command/Service/EmptyBank.pm t/Vending/Command/Service/RemoveSlot.pm t/Vending/Command/Service/Show.pm t/Vending/Command/Service/Show/Bank.pm t/Vending/Command/Service/Show/Change.pm t/Vending/Command/Service/Show/Inventory.pm t/Vending/Command/Service/Show/Money.pm t/Vending/Command/Service/Show/Slots.pm t/Vending/Content.pm t/Vending/ContentType.pm t/Vending/DataSource/CoinType.pm t/Vending/DataSource/Machine.pm t/Vending/DataSource/Machine.sqlite3-dump t/Vending/DataSource/Meta.pm t/Vending/DataSource/Meta.sqlite3-dump t/Vending/DataSource/coin_types.tsv t/Vending/Machine.pm t/Vending/MachineLocation.pm t/Vending/Merchandise.pm t/Vending/Product.pm t/Vending/ReturnedItem.pm t/Vending/Vocabulary.pm t/Vending/get_coin_by_value.pl t/Vending/machine_classes_1.uxf t/Vending/notes.txt t/Vending/t/buy_a_different_change.t t/Vending/t/buy_a_get_change_back.t t/Vending/t/buy_a_not_enough_change.t t/Vending/t/buy_b_not_enough_money.t t/Vending/t/buy_b_with_exact_change.t t/Vending/t/coin_return.t t/Vending/vend t/Vending/vend_interactive.pl t/above.t t/alternate_namespace_layout/classes/URTAlternate/Person.pm t/alternate_namespace_layout/classes/URTAlternate/Vocabulary.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/Meta.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/Meta.sqlite3-dump t/alternate_namespace_layout/data_source/URTAlternate/DataSource/TheDB.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/TheDB.sqlite3-dump t/alternate_namespace_layout/more_classes/URTAlternate/Car.pm t/alternate_namespace_layout/namespace/URTAlternate.pm t/alternate_namespace_layout/t/01_namespace.t t/alternate_namespace_layout/t/02_update_classes.t t/alternate_namespace_layout/t/02_update_classes.tar.gz t/class_browser/internal.t t/class_browser/test_namespace/Testing.pm t/class_browser/test_namespace/Testing/Color.pm t/class_browser/test_namespace/Testing/Something.pm t/class_browser/test_namespace/Testing/Something/SubClass1.pm t/class_browser/test_namespace/Testing/Something/SubClass2.pm t/newnamespace/01_command_define_namespace.t t/ur-cachetest.pl t/urbenchmark.pl ubuntu-lucid/changelog ubuntu-lucid/compat ubuntu-lucid/control ubuntu-lucid/copyright ubuntu-lucid/rules ubuntu-lucid/watch META.yml MANIFEST