PeopleSoft PeopleTools Tips & Techniques
& Techniques
About the Author Jim Marion is an AICPA Certified Information Technology Professional who currently works as a Principal Applications Technology Consultant at Oracle. He runs a popular blog for the PeopleSoft community at http://jjmpsj.blogspot.com. Jim is also an international presenter of PeopleTools development topics at conferences such as Oracle OpenWorld; UKOUG events; HEUG’s Alliance; and Quest’s, IOUG’s, and OAUG’s Collaborate.
About the Technical Editors Tim Burns is a PeopleSoft Certified Developer and has been teaching PeopleSoft technical courses since 1997. He was a technical instructor at PeopleSoft for more than seven years, where he was often recognized for the quality of his teaching. He is a Master Instructor, the highest level recognized by PeopleSoft. Tim now writes his own training manuals and teaches PeopleSoft technical courses on site at corporations, government agencies, and universities throughout the United States and abroad. Tim is well known in the PeopleSoft community and is a regular speaker at PeopleSoft conferences and user group meetings. Graham Smith is a software engineer, PeopleSoft developer, and infrastructure analyst currently working for Oxfam GB. Still in awe of the PeopleTools development framework, Graham is proud to have been asked to be part of this book project. Graham is a believer in community-built software and currently chairs the PeopleSoft Technology SIG at the UKOUG. He is a husband and father of six, enjoys fishing and trains, and fights with The Salvation Army. His PeopleSoft blog can be found at http://i-like-trains.blogspot.com.
PeopleSoft PeopleTools Tips & Techniques Jim J. Marion
New York Chicago San Francisco Lisbon London Madrid Mexico City Milan New Delhi San Juan Seoul Singapore Sydney Toronto
Copyright © 2010 by The McGraw-Hill Companies, Inc. All rights reserved. Except as permitted under the United States Copyright Act of 1976, no part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or retrieval system, without the prior written permission of the publisher. ISBN: 978-0-07-166492-9 MHID: 0-07-166492-0 The material in this eBook also appears in the print version of this title: ISBN: 978-0-07-166493-6, MHID: 0-07-166493-9. All trademarks are trademarks of their respective owners. Rather than put a trademark symbol after every occurrence of a trademarked name, we use names in an editorial fashion only, and to the benefit of the trademark owner, with no intention of infringement of the trademark. Where such designations appear in this book, they have been printed with initial caps. McGraw-Hill eBooks are available at special quantity discounts to use as premiums and sales promotions, or for use in corporate training programs. To contact a representative please e-mail us at
[email protected]. Information has been obtained by Publisher from sources believed to be reliable. However, because of the possibility of human or mechanical error by our sources, Publisher, or others, Publisher does not guarantee to the accuracy, adequacy, or completeness of any information included in this work and is not responsible for any errors or omissions or the results obtained from the use of such information. Oracle Corporation does not make any representations or warranties as to the accuracy, adequacy, or completeness of any information contained in this Work, and is not responsible for any errors or omissions. TERMS OF USE This is a copyrighted work and The McGraw-Hill Companies, Inc. (“McGrawHill”) and its licensors reserve all rights in and to the work. Use of this work is subject to these terms. Except as permitted under the Copyright Act of 1976 and the right to store and retrieve one copy of the work, you may not decompile, disassemble, reverse engineer, reproduce, modify, create derivative works based upon, transmit, distribute, disseminate, sell, publish or sublicense the work or any part of it without McGraw-Hill’s prior consent. You may use the work for your own noncommercial and personal use; any other use of the work is strictly prohibited. Your right to use the work may be terminated if you fail to comply with these terms. THE WORK IS PROVIDED “AS IS.” McGRAW-HILL AND ITS LICENSORS MAKE NO GUARANTEES OR WARRANTIES AS TO THE ACCURACY, ADEQUACY OR COMPLETENESS OF OR RESULTS TO BE OBTAINED FROM USING THE WORK, INCLUDING ANY INFORMATION THAT CAN BE ACCESSED THROUGH THE WORK VIA HYPERLINK OR OTHERWISE, AND EXPRESSLY DISCLAIM ANY WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. McGraw-Hill and its licensors do not warrant or guarantee that the functions contained in the work will meet your requirements or that its operation will be uninterrupted or error free. Neither McGraw-Hill nor its licensors shall be liable to you or anyone else for any inaccuracy, error or omission, regardless of cause, in the work or for any damages resulting therefrom. McGraw-Hill has no responsibility for the content of any information accessed through the work. Under no circumstances shall McGraw-Hill and/or its licensors be liable for any indirect, incidental, special, punitive, consequential or similar damages that result from the use of or inability to use the work, even if any of them has been advised of the possibility of such damages. This limitation of liability shall apply to any claim or cause whatsoever whether such claim or cause arises in contract, tort or otherwise.
FREE SUBSCRIPTION TO ORACLE MAGAZINE
GET YOUR
Oracle Magazine is essential gear for today’s information technology professionals. Stay informed and increase your productivity with every issue of Oracle Magazine. Inside each free bimonthly issue you’ll get:
t 6QUPEBUFJOGPSNBUJPOPO0SBDMF%BUBCBTF 0SBDMF"QQMJDBUJPO4FSWFS 8FCEFWFMPQNFOU FOUFSQSJTFHSJEDPNQVUJOH EBUBCBTFUFDIOPMPHZ BOECVTJOFTTUSFOET t 5IJSEQBSUZOFXTBOEBOOPVODFNFOUT t 5FDIOJDBMBSUJDMFTPO0SBDMFBOEQBSUOFSQSPEVDUT UFDIOPMPHJFT BOEPQFSBUJOHFOWJSPONFOUT t %FWFMPQNFOUBOEBENJOJTUSBUJPOUJQT t 3FBMXPSMEDVTUPNFSTUPSJFT
If there are other Oracle users at your location who would like to receive their own subscription to Oracle Magazine, please photocopy this form and pass it along.
Three easy ways to subscribe: 1 Web
7JTJUPVS8FCTJUFBU oracle.com/oraclemagazine :PVMMGJOEBTVCTDSJQUJPOGPSNUIFSF QMVTNVDINPSF
2 Fax
$PNQMFUFUIFRVFTUJPOOBJSFPOUIFCBDLPGUIJTDBSE BOEGBYUIFRVFTUJPOOBJSFTJEFPOMZUP+1.847.763.9638
3 Mail
$PNQMFUFUIFRVFTUJPOOBJSFPOUIFCBDLPGUIJTDBSE BOENBJMJUUP P.O. Box 1263, Skokie, IL 60076-8263
Copyright © 2008, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.
Want your own FREE subscription? To receive a free subscription to Oracle Magazine, you must fill out the entire card, sign it, and date it (incomplete cards cannot be processed or acknowledged). You can also fax your application to +1.847.763.9638. Or subscribe at our Web site at oracle.com/oraclemagazine No.
Yes, please send me a FREE subscription Oracle Magazine. From time to time, Oracle Publishing allows our partners exclusive access to our e-mail addresses for special promotions and announcements. To be included in this program, please check this circle. If you do not wish to be included, you will only receive notices about your subscription via e-mail. Oracle Publishing allows sharing of our postal mailing list with selected third parties. If you prefer your mailing address not to be included in this program, please check this circle. If at any time you would like to be removed from either mailing list, please contact Customer Service at +1.847.763.9635 or send an e-mail to
[email protected]. If you opt in to the sharing of information, Oracle may also provide you with e-mail related to Oracle products, services, and events. If you want to completely unsubscribe from any e-mail communication from Oracle, please send an e-mail to:
[email protected] with the following in the subject line: REMOVE [your e-mail address]. For complete information on Oracle Publishing’s privacy practices, please visit oracle.com/html/privacy/html
x signature (required)
date
name
title
company
e-mail address
street/p.o. box city/state/zip or postal code
telephone
country
fax
Would you like to receive your free subscription in digital format instead of print if it becomes available?
Yes
No
YOU MUST ANSWER ALL 10 QUESTIONS BELOW. 1
08014004
2
WHAT IS THE PRIMARY BUSINESS ACTIVITY OF YOUR FIRM AT THIS LOCATION? (check one only) o o o o o o o
01 02 03 04 05 06 07
o o o o o o o o o o o o o o o o o o
08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 98
Aerospace and Defense Manufacturing Application Service Provider Automotive Manufacturing Chemicals Media and Entertainment Construction/Engineering Consumer Sector/Consumer Packaged Goods Education Financial Services/Insurance Health Care High Technology Manufacturing, OEM Industrial Manufacturing Independent Software Vendor Life Sciences (biotech, pharmaceuticals) Natural Resources Oil and Gas Professional Services Public Sector (government) Research Retail/Wholesale/Distribution Systems Integrator, VAR/VAD Telecommunications Travel and Transportation Utilities (electric, gas, sanitation, water) Other Business and Services _________
3
o o o o o o o o o o o o o o o o o
99 4
99 5
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 98 o
Digital Equipment Corp UNIX/VAX/VMS HP UNIX IBM AIX IBM UNIX Linux (Red Hat) Linux (SUSE) Linux (Oracle Enterprise) Linux (other) Macintosh MVS Netware Network Computing SCO UNIX Sun Solaris/SunOS Windows Other UNIX Other None of the Above
01 02 03 04 05 06 07 o
Hardware Business Applications (ERP, CRM, etc.) Application Development Tools Database Products Internet or Intranet Products Other Software Middleware Products None of the Above
6
HARDWARE o 15 Macintosh o 16 Mainframe o 17 Massively Parallel Processing
o o o o o o
SERVICES o 24 Consulting o 25 Education/Training o 26 Maintenance o 27 Online Database o 28 Support o 29 Technology-Based Training o 30 Other 99 o None of the Above
o o
7
More than 25,000 Employees 10,001 to 25,000 Employees 5,001 to 10,000 Employees 1,001 to 5,000 Employees 101 to 1,000 Employees Fewer than 100 Employees
01 02 03 04 05 06
Less than $10,000 $10,000 to $49,999 $50,000 to $99,999 $100,000 to $499,999 $500,000 to $999,999 $1,000,000 and Over
WHAT IS YOUR COMPANY’S YEARLY SALES REVENUE? (check one only) o o o o o
9
01 02 03 04 05 06
DURING THE NEXT 12 MONTHS, HOW MUCH DO YOU ANTICIPATE YOUR ORGANIZATION WILL SPEND ON COMPUTER HARDWARE, SOFTWARE, PERIPHERALS, AND SERVICES FOR YOUR LOCATION? (check one only) o o o o o o
8
18 19 20 21 22 23
WHAT IS YOUR COMPANY’S SIZE? (check one only) o o o o o o
IN YOUR JOB, DO YOU USE OR PLAN TO PURCHASE ANY OF THE FOLLOWING PRODUCTS? (check all that apply) SOFTWARE o 01 CAD/CAE/CAM o 02 Collaboration Software o 03 Communications o 04 Database Management o 05 File Management o 06 Finance o 07 Java o 08 Multimedia Authoring o 09 Networking o 10 Programming o 11 Project Management o 12 Scientific and Engineering o 13 Systems Management o 14 Workflow
Minicomputer Intel x86(32) Intel x86(64) Network Computer Symmetric Multiprocessing Workstation Services
o o o o o o
DO YOU EVALUATE, SPECIFY, RECOMMEND, OR AUTHORIZE THE PURCHASE OF ANY OF THE FOLLOWING? (check all that apply) o o o o o o o
WHICH OF THE FOLLOWING BEST DESCRIBES YOUR PRIMARY JOB FUNCTION? (check one only) CORPORATE MANAGEMENT/STAFF o 01 Executive Management (President, Chair, CEO, CFO, Owner, Partner, Principal) o 02 Finance/Administrative Management (VP/Director/ Manager/Controller, Purchasing, Administration) o 03 Sales/Marketing Management (VP/Director/Manager) o 04 Computer Systems/Operations Management (CIO/VP/Director/Manager MIS/IS/IT, Ops) IS/IT STAFF o 05 Application Development/Programming Management o 06 Application Development/Programming Staff o 07 Consulting o 08 DBA/Systems Administrator o 09 Education/Training o 10 Technical Support Director/Manager o 11 Other Technical Management/Staff o 98 Other
WHAT IS YOUR CURRENT PRIMARY OPERATING PLATFORM (check all that apply)
01 02 03 04 05
$500, 000, 000 and above $100, 000, 000 to $500, 000, 000 $50, 000, 000 to $100, 000, 000 $5, 000, 000 to $50, 000, 000 $1, 000, 000 to $5, 000, 000
WHAT LANGUAGES AND FRAMEWORKS DO YOU USE? (check all that apply) o o o o
01 02 03 04
Ajax C C++ C#
o o o o
13 14 15 16
Python Ruby/Rails Spring Struts
10
05 Hibernate 06 J++/J# 07 Java 08 JSP 09 .NET 10 Perl 11 PHP 12 PL/SQL
o 17 SQL o 18 Visual Basic o 98 Other
WHAT ORACLE PRODUCTS ARE IN USE AT YOUR SITE? (check all that apply) ORACLE DATABASE o 01 Oracle Database 11g o 02 Oracle Database 10 g o 03 Oracle9 i Database o 04 Oracle Embedded Database (Oracle Lite, Times Ten, Berkeley DB) o 05 Other Oracle Database Release ORACLE FUSION MIDDLEWARE o 06 Oracle Application Server o 07 Oracle Portal o 08 Oracle Enterprise Manager o 09 Oracle BPEL Process Manager o 10 Oracle Identity Management o 11 Oracle SOA Suite o 12 Oracle Data Hubs ORACLE DEVELOPMENT TOOLS o 13 Oracle JDeveloper o 14 Oracle Forms o 15 Oracle Reports o 16 Oracle Designer o 17 Oracle Discoverer o 18 Oracle BI Beans o 19 Oracle Warehouse Builder o 20 Oracle WebCenter o 21 Oracle Application Express ORACLE APPLICATIONS o 22 Oracle E-Business Suite o 23 PeopleSoft Enterprise o 24 JD Edwards EnterpriseOne o 25 JD Edwards World o 26 Oracle Fusion o 27 Hyperion o 28 Siebel CRM ORACLE SERVICES o 28 Oracle E-Business Suite On Demand o 29 Oracle Technology On Demand o 30 Siebel CRM On Demand o 31 Oracle Consulting o 32 Oracle Education o 33 Oracle Support o 98 Other 99 o None of the Above
This book is dedicated to my three lovely children. To the Corrick clan and Black tribe for your tremendous encouragement in my life. To our faithful chocolate lab for keeping us company during the past six months of very late nights.
This page intentionally left blank
Contents at a Glance Part I
Core PeopleTools Concepts
1 Application Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
2 The File Attachment API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3 Approval Workflow Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4 Pagelet Wizard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Part II
Extend the User Interface
5 Understanding and Creating iScripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
6 JavaScript for the PeopleSoft Developer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
7 AJAX and PeopleSoft . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
8 Creating Custom Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Part III
Java
9 Extending PeopleCode with Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
10 A Logging Framework for PeopleCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
11 Writing Your Own Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415
12 Creating Real-Time Integrations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
13 Java on the Web Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
14 Creating Mobile Applications for PeopleSoft . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519
vii
viii
PeopleSoft PeopleTools Tips & Techniques Part IV
Best Practices
15 Test-Driven Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561
16 PeopleCode Language Arts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 579
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 595
Contents Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xix
Part I
Core PeopleTools Concepts
1 Application Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
Our First Application Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an Application Package and Class . . . . . . . . . . . . . . . . . . . . . . . . . . . Coding the Application Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Application Class Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Expanding the Application Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Features of Application Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dynamic Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Construction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stateful Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Access Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Putting It All Together: The Logging Framework Example . . . . . . . . . . . . . . . . . . . . . . . Log Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The Logger Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The Logger Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dynamic Logger Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Factory Test Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Misuses of Application Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Runtime Context-Sensitive Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Indiscriminate Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4 4 5 6 11 13 17 17 17 17 19 21 22 25 25 31 32 36 38 38 38 39 39
ix
x
PeopleSoft PeopleTools Tips & Techniques 2 The File Attachment API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Adding Attachments to Transactions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Investigating the Target Transaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Attachment Storage Record . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding the FILE_ATTACH_SBR Subrecord . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding Attachment Fields and Buttons to the Transaction Page . . . . . . . . . . . . Writing PeopleCode for the Attachment Buttons . . . . . . . . . . . . . . . . . . . . . . . . Customizing File Attachment Behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Moving to Level 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding the PeopleCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding Multiple Attachments per Transaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Processing Attachments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Accessing Attachments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Storing Attachments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementing File Attachment Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Filename Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . File Contents Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3 Approval Workflow Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Workflow-Enabling Transactions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating Supporting Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Configuring the AWE Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Transaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Approval . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Providing Custom Descriptions for the Approval Status Monitor . . . . . . . . . . . . . . . . . . Allowing Ad Hoc Access . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an Event Handler Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Web Service-Enabling Approvals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42 42 49 53 53 59 65 77 77 81 85 85 86 87 87 87 89 89 92 93 102 108 126 129 132 136 138 140
4 Pagelet Wizard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 Pagelets Defined . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Pagelet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Components of a Pagelet Wizard Pagelet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pagelet Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Setup for the Custom Data Type Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Coding the Custom Data Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Registering the Data Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Test Pagelet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pagelet Transformers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XSL Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Display Formats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
142 143 148 148 148 149 168 168 177 180 180 183 183
Contents
xi
Part II
Extend the User Interface
5 Understanding and Creating iScripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 iScripts Defined . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Our First iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Coding the iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Bookmarklet to Call an iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Writing the SetTraceSQL iScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Bookmarklet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desktop Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an iScript to Serve Calendar Content . . . . . . . . . . . . . . . . . . . . . . . . . Building a Parameter Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Transaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Serving File Attachments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iScripts as Data Sources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flex Requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Say Hello to Flex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Direct Reports DisplayShelf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
188 189 189 190 191 192 193 194 197 198 200 203 209 210 211 211 220 227 227
6 JavaScript for the PeopleSoft Developer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 A Static JavaScript Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Dynamic JavaScript Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Derived/Work Record for Dynamic HTML . . . . . . . . . . . . . . . . . . Adding PeopleCode for the HTML Area . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an HTML Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inspecting PeopleSoft’s User Interface with Firebug . . . . . . . . . . . . . . . . . . . . . . . . . . . Using Firebug’s Console . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using Firebug to Enhance the Trace Bookmarklet . . . . . . . . . . . . . . . . . . . . . . . Styling an Element . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JavaScript Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Serving JavaScript Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using jQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Making Global User Interface Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Identifying Common Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Minimizing the Impact . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Coding the Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using jQuery Plug-ins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Thickbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . WEBLIB_APT_JSL iScript Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Performance Issues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
230 233 234 234 235 236 236 237 239 240 241 243 245 245 247 247 254 254 260 261 261 261
xii
PeopleSoft PeopleTools Tips & Techniques 7 AJAX and PeopleSoft . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 Hello AJAX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the AJAX Request Handler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ajaxifying a Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding Animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ajaxifying the Direct Reports DisplayShelf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Flex Source . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Direct Reports Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a New HTML AJAX Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Container Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Configurable User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using a Metadata Repository . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Bootstrap Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Custom Scripts Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Highlight Active Field Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Changing Search Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fiddler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
264 264 264 266 268 269 270 271 273 275 275 285 291 291 293 298 301 302
8 Creating Custom Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303 The Toolbar Button Metadata Repository . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Setting Up the Repository Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Toolbar Maintenance Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Defining the Toolbar’s HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Attaching the Toolbar to Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Defining a Custom Script for the Toolbar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding the Trace Toolbar Button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifying the Bootstrap Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Separating Common Code from Bootstrap Code . . . . . . . . . . . . . . . . . . . . . . . . Adding New URL-Generation Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Writing the New Common Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Launching Another Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an iScript to Get CREF Information . . . . . . . . . . . . . . . . . . . . . . . . . . . Adding the Edit CREF Toolbar Button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Viewing Query Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Query to Get a Page’s Permission Lists . . . . . . . . . . . . . . . . . . . . . . Adding the Query Toolbar Button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Leaving the Portal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
304 304 308 311 312 312 314 316 316 317 318 324 324 325 328 328 329 333 334
Contents
xiii
Part III
Java
9 Extending PeopleCode with Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 Java Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Why Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Why Not C++ or .NET or …? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java and PeopleCode 101 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Writing a Meta-HTML Processor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementing %Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementing %JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementing %GenerateQueryContentURL . . . . . . . . . . . . . . . . . . . . . . . . . . Complete Code for the Meta-HTML Processor . . . . . . . . . . . . . . . . . . . . . . . . . Using Third-Party Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Apache Commons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Apache Velocity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
338 338 339 339 339 347 349 350 350 353 356 361 364 365 368 373 385 385
10 A Logging Framework for PeopleCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387 Investigating Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Delivered Logging Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The log4j Java Logging Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hello log4j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tracing log4j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Configuring log4j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Improving Logging Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Avoiding Logger Reconfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using log4j in the Process Scheduler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . An Integrated Logging Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Level Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Logger Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The LogManager Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . log4j Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing APT_LOG4J . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
388 388 389 389 392 393 397 397 398 398 399 400 403 410 411 413 413
xiv
PeopleSoft PeopleTools Tips & Techniques
11 Writing Your Own Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415 Your Java Build Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Your First Java Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Source Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deploying Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Test Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using PeopleCode Objects in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Configuring Your Development Environment . . . . . . . . . . . . . . . . . . . . . . . . . . Using PeopleCode System Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Accessing Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PeopleSoft Database log4j Appender . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the PL/SQL Autonomous Transaction . . . . . . . . . . . . . . . . . . . . . . . . . Writing the Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Appender . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Static Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PeopleSoft Database Velocity Template Data Source . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Template Metadata Repository . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Velocity Repository Java Class . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the PSDBResourceLoader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
416 416 416 420 426 426 426 427 429 436 437 441 448 449 450 450 453 460 461 461 462
12 Creating Real-Time Integrations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463 Integration Technologies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Setting Up for Database Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Custom JDBC Target Connector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the JDBCTargetConnector Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Predeployment Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deploying the Connector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Configuring Integrations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Configuring the Gateway . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Transforming Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Routing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Integrated Connector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Troubleshooting Custom Connectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
464 464 466 466 482 492 492 493 494 495 499 501 502 504 504
Contents
13 Java on the Web Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505 Extending the PeopleSoft Web Server with JSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using Servlet Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Investigating iScript Caching Behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating an HTTP Header Servlet Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Servlet Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deploying the Servlet Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
xv 506 507 508 509 513 515 516 517
14 Creating Mobile Applications for PeopleSoft . . . . . . . . . . . . . . . . . . . . . . . . . . 519 Providing Web Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Enabling a Component Interface as a Web Service . . . . . . . . . . . . . . . . . . . . . . Testing the WSDL URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Going Mobile with JDeveloper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating a Fusion Web Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the Data Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creating the View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Designing the Search Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing the Search Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Shortening the Application’s URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Requiring Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
520 520 528 528 529 532 536 540 553 555 556 557 558
Part IV
Best Practices
15 Test-Driven Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561 Introduction to Test-Driven Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 562 The TDD Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 562 Some TDD Lingo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 563 A TDD Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564 Test Driving the Meta-HTML Processor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564 Writing a Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564 Running the Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 566 Making the Test Pass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 569 Running the Test Again . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 569 Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570 Repeating the Cycle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 573 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 578 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 578
xvi
PeopleSoft PeopleTools Tips & Techniques
16 PeopleCode Language Arts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 579 Composition over Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 580 Façades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 581 Factories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582 Inversion of Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 583 Enumerated Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 591 Language Diversity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 593 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 595
Acknowledgments
While I claim full responsibility for the content of this book, including errors and omissions, I cannot take credit for its comprehensibility. For that, I give special thanks to my wife Sarah, who spent countless hours rephrasing and organizing my thoughts to make this book communicate effectively. She is my unsung coauthor. Likewise, Tim Burns and Graham Smith devoted six months of their lives to reading, testing, fixing, and debugging the code samples included with this book. These three people gave six months of their lives to ensure the legibility, accuracy, and integrity of this book. Rebecca and Jo, thank you so much for your extra effort at home that allowed your husbands to help me write this book. I also want to thank David Kurtz, Tom Kyte, Sheila Cepero, and Lisa McClain for helping me get this project started. Ed Abbo and John Gawkowski, thank you for approving this project. To my managers, Irina Granat and Roger Donaldson, thank you for encouraging me to write this book. Shawn Abernathy, thank you for your valuable insight into the Approval Workflow Engine. Meghan, I appreciate your accountability and dedication to this project. Vipra, Marilyn, and Patty, your style and content editing recommendations were invaluable. To the PeopleTools giants, Rich Manalang and Chris Heller, I am a dwarf standing on your shoulders. Rich, you opened my eyes to the world of AJAX and PeopleTools. Chris, you taught me the idiosyncrasies of working with PeopleTools and Java. To my PeopleTools instructors, Toby Yoches, Tim Burns, Tom Spol, and Scott Sarris. I wouldn’t know an app class from a component interface if it weren’t for your thorough instructions. To my fellow PeopleTools presenters, Dave Bain, Jeff Robbins, Greg Kelly, Robert Taylor, Peter Bergmann, and the rest of the demo grounds staff, thank you for giving me your time and energy and helping me further understand how to use and extend PeopleTools. Robert, I specifically credit you with the ideas behind the custom target connector chapter. To the Tipster (Duncan), Digital Eagle, Chili Joe, and the rest of the PeopleSoft bloggers, each of you contributed to the ideas presented in this book. To my friends at the University of Utah,
xvii
xviii
PeopleSoft PeopleTools Tips & Techniques
your tough questions and insight move me to new heights. To the PeopleSoft team at Chelan County PUD, thank you for introducing me to PeopleSoft. Thanks to all the PeopleSoft customers, consultants, and technical presales consultants who visit my blog, ask me questions at user groups, and communicate with me on a daily basis. You inspire me. Learning how you use and customize the product leads me to the solutions I create. Most important, thank you to my personal Lord and Savior Jesus Christ. Writing a computer programming manual is a monumental effort. To do it in six months, as a second job, requires divine intervention. He is the chief author of all good ideas, the creative force leading all great innovation.
Introduction
As a regular speaker on the PeopleSoft user group circuit, I have the opportunity to present PeopleTools-related tips and techniques to a variety of customers. Audiences are eager to implement the solutions demonstrated, but many people don’t know where to start. These one-hour presentations generate excitement by showing what is possible, but they are not detailed enough to teach how to properly implement the solutions. The PeopleTools community needs documentation to bridge the gap between these presentations and the actual implementation. This book’s examples will assist you in your pursuit of knowledge, whether you are looking for more information on application classes and workflow, or are trying to extend your applications with Java and AJAX. The complexity of the examples presented in this book ranges from beginner to expert. The ideas and examples were designed to teach the inexperienced, as well as inspire the seasoned veteran. Each chapter in this book describes tips and techniques for minimizing your modification footprint. By reading this book, you will learn how to combine PeopleTools with modern, wellknown languages, technologies, and methodologies. In the hands of a developer trying to solve a problem, this book is a solution generator. I packed this book with working examples that produce tools and enhancements I hope you will want to incorporate into your PeopleSoft applications.
xix
xx
PeopleSoft PeopleTools Tips & Techniques
What’s Inside The content of this book is divided into four parts: ■■ Core PeopleTools Concepts ■■ Extend the User Interface ■■ Java ■■ Best Practices I organized the chapters in each part by level of complexity. Except for the chapters in Part II, each chapter is self-contained. For continuity, some examples refer to others presented in earlier chapters. The source code for each chapter is available at www.OraclePressBooks.com. Each custom object described in this book is prefixed with the letters APT to help you distinguish your organization’s custom objects from the custom objects in this book (unless, of course, your organization also uses the prefix APT). This prefix is an abbreviation for A PeopleTools book.
Core PeopleTools Concepts Part I contains working examples of core PeopleTools technologies. In Chapter 1, you will learn application class design patterns while you build your own transparent, configurable logging framework. Application classes are relatively new to the PeopleTools toolbox, and represent a significant step forward. The concepts in this chapter form the foundation for many other chapters in this book. File attachments, workflow, and approvals are key usability features of a good transaction system. Chapters 2 and 3 describe these core PeopleTools features. Chapter 2 covers the File Attachment API by working through examples of adding attachments to transactions. Chapter 3 shows you the inner workings of the Approval Workflow Engine (AWE), which PeopleSoft introduced with PeopleTools 8.48. This chapter walks through the process of writing, configuring, and implementing a new workflow process. The Pagelet Wizard is PeopleSoft’s configurable portlet generator. This tool allows functional superusers to surface actionable business intelligence. In Chapter 4, you will learn how to extend this tool by creating new data types, display formats, and transformers.
Extend the User Interface Part II of this book dives deep into user interface development. Chapter 5 shows you how to use iScripts to extend your browser through bookmarklets, integrate with desktop calendar systems, and build rich user interfaces using Adobe Flex. In Chapter 6, you will learn how to use JavaScript and CSS to change the behavior and appearance of PeopleSoft pages. Chapter 7 introduces AJAX and demonstrates how to construct a configurable JavaScript customization framework that allows you to modify the behavior and appearance of delivered pages with zero upgrade impact. Chapter 8 builds on the previous chapters by showing you how to create a custom PeopleTools toolbar that provides access to common online tools, such as tracing and security.
Java Part III is a six-chapter journey into PeopleSoft’s tight integration with the Java language. In Chapters 9, 10, and 11, you will learn how to call Java classes from PeopleCode and how to
Contents
xxi
use PeopleCode functions from Java. These chapters include everything from how to pass data between PeopleCode and Java, to how to write, compile, and deploy your own custom Java classes. In Chapter 9, you will learn how to call the Apache Velocity Engine from PeopleCode and how to produce and consume JSON-based services using the JSON.simple Java library. Chapter 10 shows you how to use the PeopleSoft-delivered log4j logging framework. Chapter 11 builds on Chapters 9 and 10 by showing you how to create PeopleSoft repositories for Apache Velocity Engine templates and log4j logging statements. PeopleSoft’s Java roots go deeper than PeopleCode integration. Chapter 12 leaves PeopleCode and shifts into PeopleSoft’s pure Java realm. In this chapter, you will learn how to create custom Integration Broker target connectors by building a database target connector. By using custom connectors, you can replace many batch-style integrations with real-time integrations. The PeopleSoft web server is a pure Java 2 Platform, Enterprise Edition (J2EE) application server. Chapter 13 shows you how to leverage the power of J2EE through JavaServer Pages (JSP), servlets, and servlet filters, which offer the opportunity to modify application behavior without changing delivered code. Chapter 14 closes the Java section by providing a step-by-step tutorial for using Oracle’s JDeveloper and Application Development Framework (ADF) framework to create mobile solutions. While the implementation described in this chapter is specific to JDeveloper and Java, you can use the ideas and concepts with any language or technology to create PeopleSoft mobile solutions.
Best Practices Even though the entire book is devoted to best practices, Part IV wraps up this book with a specific focus on development best practices. Chapter 15 uses a code walk-through to show you how to apply test-driven development (TDD) techniques. Chapter 16 rounds out the best practices discussion by offering implementations of many object-oriented patterns, including composition over inheritance, façades, factories, and inversion of control.
PeopleTools Versions and Approach to HRMS When I started writing this book, HRMS 9.0 was the newest available HRMS version, and PeopleTools 8.49.14 was the latest PeopleTools version. The images in this book were taken from those versions. Just as I was completing this text, Oracle released HRMS 9.1 and PeopleTools 8.50. To reconcile the differences between this book’s images and the latest versions of PeopleSoft, I included notes describing various differences between PeopleTools 8.49 and 8.50. Even though HRMS is Oracle’s most popular PeopleSoft application, as much as possible, I avoided HRMS-specific transactions and references. Some features, however, such as workflow, make sense only in the context of an application-specific transaction. For those examples, I supplied two solutions: one for a custom component that you can create in your application and one that was HRMS-specific.
This page intentionally left blank
Part
I
Core PeopleTools Concepts
This page intentionally left blank
Chapter
1
Application Classes
4
PeopleSoft PeopleTools Tips & Techniques
I
f you are an application class expert, feel free to skim this chapter and quickly move on to the next chapter. On the other hand, if you are new to application classes and object-oriented programming (OOP) in general, then consider this chapter to be an OOP primer. Through the examples in this book, you will learn enough about OOP concepts to create usable, productive application classes. Many of the examples in this book will build upon the application class fundamentals presented in this chapter. So, if this material is new to you, take some time to make sure you fully understand the concepts presented. Application classes provide PeopleSoft developers with a desperately needed object-oriented complement. The OOP versus procedural debate can be very personal and intense. I don’t advocate one over the other. Rather, I believe that both work together to create excellent solutions. Used correctly, application classes can dramatically improve the legibility of your code, provide extensibility, and reduce defects. In this chapter, you will have the opportunity to create and use several application classes. Through these examples, and the accompanying explanations, you will learn how and when to use application classes.
Our First Application Class
Our first application class is a derivative of the classic Hello World example. Rather than just say “Hello,” we will create a Greeter application class for use in later chapters. Here are the basic steps to create an application class: 1. Create an application package. 2. Create an application class. 3. Code your class. 4. Test your code. The following sections provide details about each of these steps.
Creating an Application Package and Class We define application classes as child objects of application packages. These packages provide structure and qualify application class names in the same way that Java packages organize and qualify Java classes. Therefore, before we create a new application class, we need to create an application package. If you haven’t already done so, launch Application Designer (more commonly known as App Designer). To create a new application package, select File | New from the App Designer menu bar. App Designer will respond by displaying the New Definition dialog box, which contains a list of possible object types. From this dialog, choose Application Package. We must save and name our new application package before continuing. Click the Save button and name this package APT_GREETERS. With our package structure defined, we can add a new class named Greeter. To add a new class, select the APT_GREETERS package in the Application Package editor and click the Insert Application Class button. App Designer will respond by prompting you for a class name. Enter Greeter and click OK. App Designer will add
Chapter 1: Application Classes
5
FIGURE 1-1. Greeter package definition
the new child item Greeter to the APT_GREETERS package in the Application Package editor. At this point, your application package should look like Figure 1-1.
Coding the Application Class Double-click the new Greeter element to open the PeopleCode editor. With our package structure in place, we can now write some code for our first application class. Type the following code into the PeopleCode editor window: class Greeter end-class;
Unlike event-based PeopleCode, application class PeopleCode must strictly conform to a contract. The preceding code defines our application class using the reserved word class followed by the name of the class: Greeter. We conclude our definition with the key phrase end-class, followed by the PeopleCode line-terminating semicolon. All class definitions follow this same pattern.
6
PeopleSoft PeopleTools Tips & Techniques Besides naming a class, the declaration defines a class’s characteristics and behaviors. Let’s add a behavior to our class definition by creating a new method. The following listing shows a new method declaration named sayHello. class Greeter method sayHello() Returns string; end-class;
Notice this method declaration looks similar to a function declaration. Just like functions, methods can accept parameters and return values. The next listing builds on the previous listings by adding a minimal implementation for our sayHello method. App Designer requires us to provide a minimal implementation for each declared method prior to saving the class. class Greeter method sayHello() Returns string; end-class; method sayHello Return "Hello " | %OperatorId; end-method;
We now have a fully functional application class we can test.
Testing the Application Class Code PeopleTools provides us with several ways to test this code. To keep this example simple, we will use an Application Engine (App Engine) program. In Chapter 15, I will demonstrate a PeopleCode testing framework designed specifically for unit testing PeopleCode. Begin by creating a new App Designer definition, just as we did when we created our application package. From the list of definition types, select App Engine. When the App Engine program editor window appears, rename step Step01 to Test1. Then right-click Test1 and choose Insert Action. App Designer will respond by inserting a new SQL action. From the drop-down list, change the action type to PeopleCode. Save this new App Engine as APT_GREETER. Your new App Engine program should look like the one shown in Figure 1-2. With a basic structure in place, we can add some PeopleCode to test our Greeter application class. Open the PeopleCode editor by double-clicking the gray area under the PeopleCode action, and then enter the following PeopleCode: import APT_GREETERS:Greeter; Local APT_GREETERS:Greeter &greeter = create APT_GREETERS:Greeter(); Local string &message = &greeter.sayHello(); MessageBox(0, "", 0, 0, &message);
The first line of this listing imports the Greeter application class into the current PeopleCode module in a manner similar to a PeopleCode Declare directive. For this example, we used the application class’s fully qualified name. When importing several classes from the same package, however, we can use the shorthand wildcard character *, which tells the PeopleCode compiler to
Chapter 1: Application Classes
7
FIGURE 1-2. New App Engine program import all the classes in the given package. For example, we could have written our import statement this way: import APT_GREETERS:*;
The second line starts by declaring a variable of type APT_GREETERS:Greeter. Just as PeopleCode provides us with the loosely typed primitive Any, it also provides us with the loosely typed OOP complement Object. While it is syntactically correct to loosely declare this variable as type Object, the PeopleCode compiler will provide only compile-time feedback if we use strict typing. The second line also creates a new instance of the Greeter class using the create keyword. An instance is an in-memory object created from the Greeter’s application class PeopleCode definition. Besides the create keyword, PeopleCode provides two additional object-creation functions: CreateObject and CreateObjectArray. I will explain these functions later in this chapter, when we build the logging framework example. The third line executes the new sayHello method and assigns the result to a variable named &message.
8
PeopleSoft PeopleTools Tips & Techniques
Spartan Programming Spartan programming1 seeks to reduce code complexity through minimalistic coding practices. Applying this approach to our test code, we could rewrite it in two lines—one declarative and one executable—as follows: import APT_GREETERS:Greeter; MessageBox(0, "", 0, 0, (create APT_GREETERS:Greeter()).sayHello());
Spartan programming favors inlining over variable usage.2 Notice our new code example does not use any variables. By reducing our code to one executable line, we have only a single line to read, comprehend, maintain, and test. After removing all the “fluff” from our code logic, flaws are easier to spot. Unfortunately, terse code like this can be difficult to read. Now, with our code reduced to the absolute minimum and any potential logic flaws exposed, let’s expand it just a little to make it easier to read,3 as follows: import APT_GREETERS:Greeter; Local APT_GREETERS:Greeter &g = create APT_GREETERS:Greeter(); MessageBox(0, "", 0, 0, &g.sayHello());
As Albert Einstein said, “Any fool can make things bigger, more complex, and more violent. It takes a touch of genius—and a lot of courage—to move in the opposite direction.”4
The last line writes the results to the App Engine log using the PeopleCode MessageBox function. We are finished coding this test. Save your work and close the PeopleCode editor. Before running this code, we should disable the App Engine’s Restart property. App Engine programs are restartable by default. When an App Engine program fails, PeopleSoft will save the run state of the App Engine so it can be restarted at a later time. Since our program is not restartable, checking this box will save us from needing to manually restart a failed instance of the program. To disable restart, with the APT_GREETER App Engine program open, press alt-enter. App Designer will display the App Engine Program Properties dialog. Switch to the Advanced tab of this dialog and check the Disable Restart check box, as shown in Figure 1-3. With our test program defined, we can save it and then run it from App Designer. To run the program, click the Run Program button on the App Engine toolbar (see Figure 1-4). App Designer will launch the Run Request dialog. Enable the Output Log to File check box. Before clicking the OK button, copy the name of the log file to your clipboard so you can easily open it in the next step. Figure 1-5 shows the Run Request dialog with all of the appropriate settings. After clicking the OK button, a new, minimized DOS window should briefly appear in your taskbar and then disappear. To see this program’s results, open the log file referenced in the Run
Chapter 1: Application Classes
FIGURE 1-3. Disabling the Restart property
Request dialog (Figure 1-5), whose name you copied to your clipboard. The contents of the file should look something like this: ... PeopleTools version and App Engine details ... 11.54.36 .(APT_GREETER.MAIN.Test1) (PeopleCode) Hello PS (0,0) Message Set Number: 0 Message Number: 0 Message Reason: Hello PS (0,0) (0,0) Application Engine program APT_GREETER ended normally 11.54.36 Application Engine ended normally
Because I was logged into App Designer as PS, the program printed Hello PS.
9
10
PeopleSoft PeopleTools Tips & Techniques
FIGURE 1-4. Click the Run Program button to open the Run Request dialog.
FIGURE 1-5. Run Request dialog settings for the test program
Chapter 1: Application Classes
11
Expanding the Application Class Let’s add a method to our Greeter class that can greet anyone, not just the logged-in user. Note The changes to our Greeter class are shown in bold text. class Greeter method sayHello() Returns string; method sayHelloTo(&name As string) Returns string; end-class; method sayHello /+ Returns String +/ Return "Hello " | %OperatorId; end-method; method sayHelloTo /+ &name as String +/ /+ Returns String +/ Return "Hello " | &name; end-method;
Embedded Strings Our Greeter class contains a string literal. If my PeopleSoft implementation serves a multilingual audience, I should store the string literal, “Hello,” in the message catalog. The message catalog allows developers to store strings by language and maintain those strings online. This allows you to avoid modifying code to update strings. Furthermore, using the message catalog’s parameter syntax eliminates messy string/variable concatenations. Here is an example that uses the message catalog: MsgGetText(21000, 2, "Hello %1 [default message]", %OperatorId);
In this example, message set 21,000 entry 2 contains the following: Hello %1
PeopleCode provides three functions for accessing text stored in a message catalog entry: MsgGet, MsgGetText, and MsgGetExplainText. MsgGet and MsgGetText both return the main text of a message catalog entry, with the difference being that MsgGet appends the message set and entry numbers to the end of the string. MsgGetExplainText returns the longer explain text associated with the message catalog entry. All three of these functions accept a default message as a parameter. Often, you will see the text Message not found used as the default message. Avoid this practice. This generic default message offers no help to your users. If you hard-code the message set and entry number, then you should have a reasonable understanding of the message’s intent—enough understanding to create a more informative default message. When creating your own message sets, use the range 20,000 through 32,767. PeopleSoft reserves the right to use message sets numbered from 1 to 19,999.
12
PeopleSoft PeopleTools Tips & Techniques
Naming Classes and Methods When naming classes and methods, I use a mixture of uppercase and lowercase letters. I begin class names with uppercase letters and method names with lowercase letters. If a name contains multiple words, I capitalize the first letter of each successive word. For example, I will name an application class for processing workflow transactions WorkflowTransactionProcessor. If that class contains a method for processing transactions, I will name that method processTransactions. I use a similar naming convention for properties and methods with one key difference: I begin method names with verbs that demonstrate the method’s action. I adapted this coding convention from those recommended by Sun.5 App Designer does not enforce naming conventions. Feel free to use whatever conventions suit your organization.
By now, I am sure you noticed the new /+ +/ comment style. PeopleCode reserves this comment style for application classes. App Designer regenerates these comments each time it validates an application class and will overwrite any modifications you make to them. The only difference between our sayHello and sayHelloTo methods is the name variable. We could centralize our string concatenation logic by calling sayHelloTo from sayHello. Let’s refactor sayHello to see how this looks. Note Changes are shown in bold text. method sayHello /+ Returns String +/ Return %This.sayHelloTo(%OperatorId); end-method;
Note Code refactoring is a technique used by software developers to improve on the internal structure of a program without modifying the program’s external behavior. Refactoring involves iterative, incremental changes that make the internal code easier to comprehend. Our refactored sayHello method introduces the system variable %This. %This is a reference to the current, in-memory instance of the Greeter class and qualifies the sayHelloTo method as belonging to this class. It is akin to the Visual Basic Me or the Java this.
Chapter 1: Application Classes
13
Inheritance
Rather than greeting a user by operator ID, it would be more appropriate for the Greeter class to address users by name. Since PeopleSoft offers several ways to derive a user’s name, rather than modify the Greeter class, let’s use PeopleCode’s inheritance feature to implement this modified behavior. Inheritance allows us to create a new class that inherits the behaviors we want to keep and to override the behaviors we want to change. We have two options for implementing this modified behavior within our new class: ■■ Write a new sayHello method that returns sayHelloTo("User's Full Name"). ■■ Create a new sayHelloTo method that expects an operator ID rather than a name. Let’s choose the latter option to ensure our new class always greets users by name. Because our new class will use the PeopleTools user profile information to look up names, let’s call it UserGreeter. To begin, open the APT_GREETERS application package and add the new application class UserGreeter. Your application package should now resemble Figure 1-6.
FIGURE 1-6. Application package with the new UserGreeter class
14
PeopleSoft PeopleTools Tips & Techniques Next, open the UserGreeter PeopleCode editor and add the following code: import APT_GREETERS:Greeter; class UserGreeter extends APT_GREETERS:Greeter method sayHelloTo(&oprid As string) Returns string; end-class; method sayHelloTo /+ &oprid as String +/ /+ Returns String +/ /+ Extends/implements APT_GREETERS:Greeter.sayHelloTo +/ Local string &name; SQLExec("SELECT OPRDEFNDESC FROM PSOPRDEFN WHERE OPRID = :1", &oprid, &name); Return %Super.sayHelloTo(&name); end-method;
SQL in PeopleCode For simplicity, UserGreeter embeds a SQL statement as a string literal. Avoid this technique by substituting SQL definitions for string literals. SQL definitions are managed objects that developers can maintain and search independent of the code that uses them. Unlike strings in PeopleCode, you can identify fields and records used in SQL definitions using the Find Definition References feature. The SQL statement in UserGreeter uses SQL bind variables. Alternatively, I could rewrite this embedded SQL using concatenation: SQLExec("SELECT OPRDEFNDESC FROM PSOPRDEFN WHERE OPRID = '" | &oprid | "'", &name);
Avoid this technique! When you concatenate a SQL statement, you create a potential SQL injection vulnerability. Consider what could happen: REM ** Runs on Oracle and returns the first matching row; sayHelloTo("Y' OR 'X' = 'X");
or REM ** Fails on Oracle, but other DB's?; sayHelloTo("PS'; DELETE FROM SECRET_AUDIT_TBL WHERE 'X' = 'X");
Through code review, you might catch this blatant attack. But what if the value passed to sayHelloTo comes from data entered by a user? Besides providing security, bind variables may offer better performance. Before executing a SQL statement, the database will create an optimal execution plan for that statement and store the compiled execution plan for later reuse.
Chapter 1: Application Classes
15
This new application class, UserGreeter, extends the base class Greeter. This means that UserGreeter inherits all the methods and properties of Greeter. Using the extends keyword, we can extend a base class by adding new methods or properties, and we can redefine the behavior of a base class by redeclaring methods defined in the base class. Notice UserGreeter.sayHelloTo(&name) uses the %Super system variable. Whereas %This represents the host object, the instance of the object executing some code, %Super represents that host’s parent class, more properly known as the object’s superclass. %Super always points to the class the current class extends. Our new sayHelloTo method does not really override the original sayHelloTo implementation. Rather, it changes the behavior of the method by executing a few lines of code prior to executing the original implementation. In this scenario, we could describe our extension as a wrapper around the original implementation. To test this new application class, we will add some code to the APT_GREETER App Engine program. Open APT_GREETER and add a new step and action. App Designer should automatically name the step Test2. Change the step’s action type to PeopleCode and save the program. Your App Engine program should now resemble Figure 1-7.
FIGURE 1-7. App Engine with new step
16
PeopleSoft PeopleTools Tips & Techniques Double-click this new PeopleCode action to open the PeopleCode editor and add the following code. import APT_GREETERS:UserGreeter; Local APT_GREETERS:UserGreeter &greeter = create APT_GREETERS:UserGreeter(); REM ** Test the inherited sayHello() method; MessageBox(0, "", 0, 0, &greeter.sayHello()); REM ** Test sayHelloTo(); Local SQL &cursor = CreateSQL("SELECT OPRID FROM PSOPRDEFN"); Local string &oprid; Local number &row = 0; While (&cursor.Fetch(&oprid) And &row < 10) &row = &row + 1; MessageBox(0, "", 0, 0, &greeter.sayHelloTo(&oprid)); End-While; &cursor.Close();
Let’s run this test and see what happens. Specifically, I am curious what will happen when we call sayHello on an application class that doesn’t define sayHello. When you run this program, you should see output that resembles the following: ... PeopleTools version and App Engine details ... 21.53.43 .(APT_GREETER.MAIN.Test1) (PeopleCode) ... 21.53.43 .(APT_GREETER.MAIN.Test2) (PeopleCode) Hello PS Message Message Message
(0,0) Set Number: 0 Number: 0 Reason: Hello PS (0,0) (0,0)
Hello [PS] Peoplesoft Superuser (0,0) Message Set Number: 0 Message Number: 0 Message Reason: Hello [PS] Peoplesoft Superuser (0,0) (0,0)... Application Engine program APT_GREETER ended normally 21.53.44 Application Engine ended normally
Calling sayHello worked! We can call sayHello because UserGreeter inherits sayHello from its superclass. Notice that Greeter.sayHello calls %This.sayHelloTo. From the results, we see that, when subclassed, %This in Greeter refers to an instance of UserGreeter. The ability to inherit and/or override a class’s behavior is a powerful feature of OOP.
Chapter 1: Application Classes
17
Features of Application Classes
Now that you have created some application classes, let’s dive deeper into the features and constructs that differentiate application classes from traditional event-based PeopleCode.
Dynamic Execution The discussion thus far demonstrates functionality that we could just as easily write using procedural code rather than object-oriented code. But with procedural code, you cannot modify runtime behavior without modifying design-time code. Using interfaces, subclasses, and PeopleCode objectcreation functions, you can dynamically execute PeopleCode without knowing the implementation at design time. Through the creative use of PeopleCode and metadata, this flexibility allows us to wire together business rules through configuration. The Pagelet Wizard offers an excellent example of this. Oracle delivers the Pagelet Wizard with several data sources and display types implemented as application classes. At design time, the Pagelet Wizard knows about interfaces, but not implementations. The actual runtime behavior of the Pagelet Wizard is wired together at runtime using metadata configured by online users. It is this same feature of application classes that allows for online configuration of Integration Broker message handlers, AWE workflow handlers, and Enterprise Portal branding elements.
Construction Our UserGreeter PeopleCode serves as a blueprint that defines the characteristics and behavior of a PeopleCode object. There may be several instances of a UserGreeter in memory at a given time, but there is only one class definition. The process for creating an in-memory object from a PeopleCode application class is called instantiation. Instantiation occurs when PeopleCode uses the create keyword or one of the CreateObject or CreateObjectArray PeopleCode functions. The first phase of instantiation is the construction phase. At construction, the PeopleCode processor executes a special application class method called the constructor. The constructor is a method that has the same name as the application class and does not return a value. Like other methods, constructors can accept parameters. The constructor is responsible for initializing an object, and therefore should contain the parameters necessary to create a valid, usable object.
Stateful Objects Just like physical objects, application classes have state. An object’s state represents the characteristics of that object. For example, an eight-year-old male chocolate Labrador Retriever could be described as a dog having the following state: Breed: Labrador Retriever Color: chocolate Age: 8 years Sex: male The state of an application class is maintained in properties and instance variables. Properties represent the externally visible characteristics of an application class, whereas instance variables
18
PeopleSoft PeopleTools Tips & Techniques represent the object’s internal state. Properties comprise a portion of an application class’s external interface. Instance variables do not. Consider the following application class declaration: REM ** code for class APT_LOG4PC:Loggers:Logger; class Logger method info(&msg As string); method debug(&msg As string); property number level; end-class;
Note The code listings in this section are intended for explanatory purposes only. Unlike prior examples, you are not expected to execute this code directly. After this section, which describes various features of application classes, we will work through a hands-on example of using application classes to build a logging framework. The preceding Logger class declaration contains one read/write property called level. The following code adds a read-only property to this class: class Logger method info(&msg As string); method debug(&msg As string); property number level; property string lastMessage readonly; end-class;
The keyword readonly following the lastMessage property marks the property as read-only. This means that the application class declaring this property can modify the property’s value, but PeopleCode acting on an instance of this class may only read its value. Changing a property’s value often triggers a change in the internal state of an application class. For example, if the filename of a FileLogger instance changes, then the application class may need to close the current file to open a new file. You can use the set modifier to execute a block of PeopleCode when a property value changes. import APT_LOG4PC:Loggers:Logger; class FileLogger extends APT_LOG4PC:Loggers:Logger method info(&msg As string); method debug(&msg As string); property File logFile; property number level; property string fileName get set; ... end-class;
Chapter 1: Application Classes
19
set fileName /+ &NewValue as String +/ end-set; get filename ... end-get;
Some properties don’t have an in-memory representation. The following code block derives a value for the read-only property isDebugEnabled: Property boolean isDebugEnabled get; ... get isDebugEnabled return (%This.level > = 100); end-get;
Note The preceding code listing uses an inline expression to return a true or a false value. As with any operation, the parentheses surrounding the comparison, %This.level > = 100, tell the PeopleCode runtime to evaluate the statement prior to returning a value. The sample statement returns true if the value in %This.level is greater than or equal to 100. You can define properties as any PeopleCode type. The get, set, and readonly modifiers define a property’s mutability (the ability of code acting on a class to change the value of the property). Without any modifiers, a property is defined as read/write. A property defined with the modifier combination get set is also a read/write property, with the subtle difference that the PeopleCode runtime executes a block of PeopleCode. This difference provides the application class with the opportunity to validate a value prior to mutation or derive a value prior to access. The readonly and get/set modifiers are mutually exclusive. If you specify get, then you cannot use readonly. You can combine the get and set modifiers or use the get modifier exclusively. The set modifier must be accompanied by the get modifier.
Access Control Application classes typically contain stateful information that should be accessed solely by the application class definition. For example, our FileLogger class has a File object. We should not allow access to this object outside this application class. What if someone executed the Close method on our open File? These sorts of mutations may introduce undetectable side effects. PeopleCode allows for three levels of access control: ■■ Public Public instance variables, called properties, are defined as property number level;. You have already seen many examples of public method definitions. Combined, public methods and properties form the external interface of an application class. ■■ Protected Protected properties and methods are methods that can be accessed by the defining class and subclasses, but not by PeopleCode outside the class’s inheritance structure.
20
PeopleSoft PeopleTools Tips & Techniques ■■ Private Private access control is the most restrictive. Because properties define externally accessible characteristics of an object, we cannot define private properties. Rather, we maintain the private state of an application class using instance variables. Private methods are just like public and protected methods, except that private methods can be accessed only by the declaring class. These access control modifiers apply to methods, properties, and instance variables, collectively referred to as members. The accessibility of a method or property is determined by its placement in an application class declaration. The following listing provides the syntax for a class declaration in Extended Backus–Naur Form (EBNF), which is an ISO standard syntax for describing computer programming languages. Class class class_name [[extends | implements] base_class_name] [{method_declarations}] [{property_declarations}] [protected [{method_declarations}] [{instance_declarations}] [{constant_declarations}]] [private [{method_declarations}] [{instance_declarations}] [{constant_declarations}]] end-Class;
Methods method_name([Parm1 [{, Parm2. . .}]]) [Returns Datatype] [abstract];
Properties property DataType property_name [get [set] | readonly] | [abstract];
To summarize, you define public methods and properties directly under the class name, protected properties and methods under the protected label, and private methods and instance variables under the private label. Some object-oriented languages do not use formal property declarations. These languages expose an object’s internal state using an accessor/mutator6 design pattern. For example, if we wrote FileLogger in Java, rather than creating a fileName property, we would create an accessor method named getFileName() and a mutator method named setFileName(String parm). By convention, methods named getXxx return the value of a property (accessor), and methods named setXxx modify a property (mutator). These conventions are very similar to the PeopleCode get and set modifiers used to declare application class properties.
Chapter 1: Application Classes
21
If you come to PeopleCode from another object-oriented language, you may prefer the accessor/mutator design pattern. PeopleBooks discourages the use of accessor/mutator methods in favor of property declarations, claiming a slight performance gain from avoiding a method call for each property change.7 Nevertheless, I prefer the getXxx and setXxx convention used by other object-oriented languages, because these methods communicate the intent of the application class author. Using PeopleBooks’ recommendation, the only way to differentiate a property from a method is context: ■■ Parentheses will never follow a property. ■■ A property may be used on the left side of an equal sign. Similarly, without peeking at an application class’s declaration, it is impossible to determine the mutability of a property. On the other hand, by composing the entire external interface of methods, I do not need to consider the differences between properties and methods, and can treat them all as methods. Furthermore, property definitions do not lend themselves to the design of fluent interfaces, which we will discuss later in this chapter.
Putting It All Together: The Logging Framework Example
A common PeopleCode debugging practice involves inserting MessageBox statements at strategic locations within a PeopleCode segment. Unlike trace files, this technique allows you to choose what and how statements are logged. This is a very effective technique for peeking into the state of an application at runtime. But what happens to those print statements when you finally discover the code’s purpose and resolve any related errors? You delete them, right? Imagine the support calls you would receive if you left those cryptic debug messages in your code! However, although it is effective, this approach suffers from two problems: ■■ When you’re finished debugging, you need to find and remove all those MessageBox statements. What if you miss one? ■■ If the original problem was data-related, then your functional team may call on you to perform this same investigation again. Finding the right combination of debug statements requires thought and effort. It is a shame to waste that effort by deleting those statements. It would be great if we could place those MessageBox statements in our code and leave them there, turning them on and off as needed. A logging framework provides a solution to this type of problem. Let’s take the Logger class introduced earlier and convert it into a foundation for an extensible logging framework. Files are common targets for logging because writing to a file does not interfere with the user interface. From a user’s perspective, file logging is transparent. This discussion, however, focuses on a very obtrusive user interface logging technique. Sometimes this obtrusive behavior is preferred. Based on this information, we know we want two logging targets: ■■ A file target called FileLogger ■■ A message box target called MessageBoxLogger
22
PeopleSoft PeopleTools Tips & Techniques
Log Levels We will want multiple logging levels, as well as corresponding print methods. When setting the level to debug, we want the logging framework to print debug and fatal messages. When the level is set to fatal, however, we want the framework to print only fatal messages. The following code listing contains the code for a Level class. This class has a string representation for printing descriptions and a numeric representation for inclusive comparisons. Following OOP best practices, the Level class encapsulates all the logic related to logging levels. The Level class is the most complex of all our framework classes because it is responsible for determining what to log. Create a new application package named APT_LOG4PC and add a new class named Level. Save the package and add the following code to the Level class. class Level method enableForFatal() Returns APT_LOG4PC:Level; method enableForDebug() Returns APT_LOG4PC:Level; method enableFor(&descr As string, &level As number) Returns APT_LOG4PC:Level; method enableFromNumber(&level As number) Returns APT_LOG4PC:Level; method toNumber() Returns number; method toString() Returns string; method isFatalEnabled() Returns boolean; method isDebugEnabled() Returns boolean; method isEnabledFor(&level As APT_LOG4PC:Level) Returns boolean;
private Constant Constant Constant Constant
&LEVEL_FATAL = 500; &LEVEL_DEBUG = 100; &LEVEL_FATAL_DESCR = "FATAL"; &LEVEL_DEBUG_DESCR = "DEBUG";
instance number &levelNumber_; instance string &levelDescr_; method setLevel(&descr As string, &nbr As number) Returns APT_LOG4PC:Level; end-class; method enableForFatal /+ Returns APT_LOG4PC:Level +/ Return %This.setLevel(&LEVEL_FATAL_DESCR, &LEVEL_FATAL); end-method;
Chapter 1: Application Classes method enableForDebug /+ Returns APT_LOG4PC:Level +/ Return %This.setLevel(&LEVEL_DEBUG_DESCR, &LEVEL_DEBUG); end-method; method enableFor /+ &descr as String, +/ /+ &level as Number +/ /+ Returns APT_LOG4PC:Level +/ Return %This.setLevel(&descr, &level); end-method; method enableFromNumber /+ &level as Number +/ /+ Returns APT_LOG4PC:Level +/ Local APT_LOG4PC:Level &this; Evaluate &level When = &LEVEL_FATAL &this = %This.enableForFatal(); When = &LEVEL_DEBUG &this = %This.enableForDebug(); When-Other &this = %This.enableFor("UNKNOWN", &level); End-Evaluate; Return %This; end-method; method isFatalEnabled /+ Returns Boolean +/ Return (&LEVEL_FATAL >= %This.toNumber()); end-method; method isDebugEnabled /+ Returns Boolean +/ Return (&LEVEL_DEBUG >= %This.toNumber()); end-method; method isEnabledFor /+ &level as APT_LOG4PC:Level +/ /+ Returns Boolean +/ Return (&level.toNumber() >= %This.toNumber()); end-method; method toString /+ Returns String +/ Return &levelDescr_; end-method;
23
24
PeopleSoft PeopleTools Tips & Techniques method toNumber /+ Returns Number +/ Return &levelNumber_; end-method; method setLevel /+ &descr as String, +/ /+ &nbr as Number +/ /+ Returns APT_LOG4PC:Level +/ &levelDescr_ = &descr; &levelNumber_ = &nbr; Return %This; end-method;
Notice that many of the methods in the Level class return a Level object. These methods don’t actually create a new Level. Rather, they return the object itself. This technique facilitates method chaining, a pattern that allows us to chain multiple method calls in a single statement. We will leverage this technique when we implement a Logger.
Fluent Interface Design Eric Evans and Martin Fowler coined the term fluent interfaces8 to describe a design pattern that strives to make code easier to read. By interface, I’m referring to the external facing design of an application class (public and protected members), not the interface class type. A fluent method returns a reference to the object that is most likely to be used next, allowing a programmer to chain the next method call to the previous call. This methodchaining9 technique is a key component of fluent interface design. For example, our Logger will have a setLevel method that takes a Level object as a parameter. Typical object-oriented code to set the log level would look something like this: Local APT_LOG4PC:Level &debugLevel = create APT_LOG4PC:Level(); &debugLevel.enableForDebug(); &logger.setLevel(&debugLevel);
This code requires a temporary variable to hold the Level object just so we can set its value. If we could create and configure the Level object in one statement, we could eliminate the temporary variable assignment. Here is the same code rewritten using method chaining: &logger.setLevel((create APT_LOG4PC:Level()).enableForDebug());
Now, arguably, some might suggest that all of the parentheses required for a PeopleCode constructor make this example a little less fluent than other examples. In this book, you will see several examples of method chaining to create fluent interfaces. In fact,
Chapter 1: Application Classes
25
when we discuss JavaScript and user interface coding, you will see extensive use of method chaining with the jQuery library. When using method chaining with PeopleCode, it is very important that you think about fluent interface design. Unlike other languages, PeopleCode doesn’t allow you to ignore return values. Rather, you may find that method chaining requires you to create temporary variables where you wouldn’t otherwise need them. For example, calling the enableForDebug method on a Level object already assigned to a local variable would require you to assign the return value to another temporary variable as follows: Local APT_LOG4PC:Level &debugLevel = create APT_LOG4PC:Level(); Local Object &temp = &debugLevel.enableForDebug();
When used correctly, method chaining should improve the legibility of your code without requiring unnecessary temporary variables. It is this improved readability that lead Martin Fowler and Eric Evans to name this technique fluent interfaces.
The Logger Interface The following listing defines an interface that both FileLogger and MessageBoxLogger will implement: import APT_LOG4PC:Level; interface Logger /* Statement logging methods, one for each level */ method fatal(&msg As string); method debug(&msg As string); /* Convenience methods for determining the current log level */ method isFatalEnabled() Returns boolean; method isDebugEnabled() Returns boolean; method isEnabledFor(&level As APT_LOG4PC:Level) Returns boolean; method setLevel(&level As APT_LOG4PC:Level); end-interface;
The Logger Classes I foresee more similarities than differences between Logger implementations. To ensure we hold to the DRY (don’t repeat yourself) principle, we’ll create a base abstract class that consolidates common logic. This consolidation allows Logger implementations to focus on the one feature that makes each Logger unique. The intent of this inheritance is to make our final implementation classes easier to write, maintain, and comprehend.
26
PeopleSoft PeopleTools Tips & Techniques
The LoggerBase Class The following listing contains code for the LoggerBase class, which both FileLogger and MessageBoxLogger will extend. import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Level; class LoggerBase implements APT_LOG4PC:Loggers:Logger method LoggerBase(); REM ** Statement logging methods, one for each level; method fatal(&msg As string); method debug(&msg As string); REM ** method method method
Convenience methods to determine the current log level; isFatalEnabled() Returns boolean; isDebugEnabled() Returns boolean; isEnabledFor(&level As APT_LOG4PC:Level) Returns boolean;
method setLevel(&level As APT_LOG4PC:Level); protected method writeToLog(&level As APT_LOG4PC:Level, &msg As string) abstract; private instance APT_LOG4PC:Level &level_; end-class; method LoggerBase &level_ = (create APT_LOG4PC:Level()).enableForFatal(); end-method; method fatal /+ &msg as String +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.fatal +/ If (%This.isFatalEnabled()) Then %This.writeToLog( (create APT_LOG4PC:Level()).enableForFatal(), &msg); End-If; end-method; method debug /+ &msg as String +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.debug +/ If (%This.isDebugEnabled()) Then %This.writeToLog( (create APT_LOG4PC:Level()).enableForDebug(), &msg); End-If; end-method;
Chapter 1: Application Classes
27
REM ** Convenience methods to determine the current log level; method isFatalEnabled /+ Returns Boolean +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.isFatalEnabled +/ Return &level_.isFatalEnabled(); end-method; method isDebugEnabled /+ Returns Boolean +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.isDebugEnabled +/ Return &level_.isDebugEnabled(); end-method; method isEnabledFor /+ &level as APT_LOG4PC:Level +/ /+ Returns Boolean +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.isEnabledFor +/ Return &level_.isEnabledFor(&level); end-method; method setLevel /+ &level as APT_LOG4PC:Level +/ /+ Extends/implements APT_LOG4PC:Loggers:Logger.setLevel +/ &level_ = &level; end-method;
The previous listing declares LoggerBase as an application class that implements the Logger interface. This relationship identifies LoggerBase as a Logger, meaning instances of LoggerBase can be assigned to variables declared as type Logger. Notice that LoggerBase declares and implements every method defined by the Logger interface. A class that implements an interface must implement (or declare as abstract) every method defined by the interface. Look closely at the writeToLog method declaration. Notice that it is declared in the protected section. By placing this method in the protected members section, we tell the PeopleCode compiler to hide this method from PeopleCode running outside this class’s inheritance hierarchy. Only this class and its subclasses can access this method. We also use the keyword abstract to qualify the writeToLog method. This keyword tells the PeopleCode compiler that LoggerBase does not implement this method. A class with an abstract method is called an abstract class. Subclasses of abstract classes must either provide an implementation for abstract methods or redeclare those methods as abstract. We cannot create instances of abstract classes directly. Rather, we create instances of abstract classes through their subclasses. LoggerBase is our first application class with a constructor. As mentioned earlier in the chapter, constructors are methods with the same name as the host class—LoggerBase in this case. You need to define a constructor in these cases: ■■ You need to initialize the application class prior to use. ■■ Your application class requires parameters. ■■ You are subclassing another class whose constructor requires parameters.
28
PeopleSoft PeopleTools Tips & Techniques In our case, we declared a constructor to initialize the Level instance variable &level_. The LoggerBase constructor uses method chaining: &level_ = (create APT_LOG4PC:Level()).enableForFatal();
Without method chaining, we would write this statement as follows: &level_ = create APT_LOG4PC:Level(); &level_.enableForFatal();
Sometimes method chaining makes code easier to read, and sometimes it doesn’t. The way you use method chaining is part of fluent interface design. Notice that the LoggerBase debug and fatal methods execute the abstract writeToLog method. To implement logging functionality, subclasses of LoggerBase just need to implement the writeToLog method. This frees our Logger implementations to focus on their differentiating factor, which is logging. One of our requirements is to be able to turn logging on and off through configuration. By delegating the on/off decision to the Level class, the LoggerBase class does not need to know the relationship between logging levels, because that is an implementation detail of the Level class. This design decision follows the practice of encapsulation, which suggests that other objects should not know the implementation details of an object.10 You can see this delegation in the isFatalEnabled, isDebugEnabled, and isEnabledFor methods.
The MessageBoxLogger Class The following code listing contains the implementation for a logger that writes messages to the PeopleCode MessageBox function, creatively named MessageBoxLogger. import APT_LOG4PC:Loggers:LoggerBase; import APT_LOG4PC:Level; class MessageBoxLogger extends APT_LOG4PC:Loggers:LoggerBase protected method writeToLog(&level As APT_LOG4PC:Level, &msg As string); end-class; method writeToLog /+ &level as APT_LOG4PC:Level, +/ /+ &msg as String +/ /+ Extends/implements APT_LOG4PC:Loggers:LoggerBase.writeToLog +/ MessageBox(0, "", 0, 0, &level.toString() | ": " | &msg); end-method;
With the boilerplate code handled by the LoggerBase superclass, this code is free to focus on the one feature that makes this Logger implementation unique. Through inheritance, the MessageBoxLogger class contains all the methods of the LoggerBase superclass, as well as the one writeToLog method that is declared here.
Chapter 1: Application Classes
29
Composition Versus Inheritance We built loggers using OOP inheritance: A FileLogger inherits from LoggerBase, which implements Logger. Inheritance suffers several design and implementation problems. For example, if you want to format strings prior to logging them, what class would you change? Would you implement this behavior in the LoggerBase class? If so, how would you propagate this change to all subclasses? What if one subclass can’t interpret the strings formatted by this new string formatting routine? Composition provides an alternative to inheritance. Using composition, we flatten the LoggerBase and Logger interface into a single Logger class and delegate printing to implementations of a Target interface. This one Logger class could then print to any destination using a runtime configured Target. If we wanted to create a formatter class for formatting strings, we could create a class that implements the Target interface and then delegates printing to another Target instance. This book provides several more examples of both inheritance and composition. Chapter 15 covers a design technique called test-driven development, which provides many opportunities to replace inheritance with composition.
Notice the MessagBoxLogger class does not have a constructor. If you extend a class whose constructor does not require parameters, and your only purpose for a constructor is to set the value of %Super, you do not need a constructor.11
The FileLogger Class Another useful logging target is a file logging target. Rather than obnoxiously displaying messages, a FileLogger transparently writes messages to a file. The following code listing contains the implementation for this logger. import APT_LOG4PC:Loggers:LoggerBase; import APT_LOG4PC:Level; class FileLogger extends APT_LOG4PC:Loggers:LoggerBase method FileLogger(&fileName As string); protected method writeToLog(&level As APT_LOG4PC:Level, &msg As string); private instance File &logFile; end-class; method FileLogger /+ &fileName as String +/ %Super = create APT_LOG4PC:Loggers:LoggerBase(); &logFile = GetFile(&fileName, "A", "A", %FilePath_Absolute); end-method;
30
PeopleSoft PeopleTools Tips & Techniques method writeToLog /+ &level as APT_LOG4PC:Level, +/ /+ &msg as String +/ /+ Extends/implements APT_LOG4PC:Loggers:LoggerBase.writeToLog +/ &logFile.WriteLine(&level.toString() | ": " | &msg); end-method;
The FileLogger constructor declaration requires a string parameter. Since the purpose of this application class is to write messages to a file, this object would not be valid without this target filename. Put another way, each of the write methods of this logger would fail in the absence of a target filename. We use our constructor to initialize our object, placing it in a usable state. The constructor creates an instance of the superclass and assigns it to the system variable %Super. When implementing a constructor for a class that extends another class, that constructor must assign a value to %Super. If you don’t define a constructor for a subclass, then the PeopleCode runtime will create an instance of the superclass and assign it to %Super. Figure 1-8 shows the package structure of the APT_LOG4PC application package.
FIGURE 1-8. APT_LOG4PC package structure
Chapter 1: Application Classes
31
Testing the Framework With the code written, we can test this mini logging framework. Now create an App Engine program with a PeopleCode action and enter the following code into the PeopleCode editor: import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Loggers:MessageBoxLogger; import APT_LOG4PC:Level; Local APT_LOG4PC:Level &level = create APT_LOG4PC:Level(); Local APT_LOG4PC:Loggers:MessageBoxLogger &logger = create APT_LOG4PC:Loggers:MessageBoxLogger(); &logger.setLevel(&level.enableForDebug()); &logger.fatal("This is for DEBUG level"); &logger.debug("This is for DEBUG level"); &logger.setLevel(&level.enableForFatal()); &logger.fatal("This is for FATAL level"); &logger.debug("This is for FATAL level");
When you run this program, you should see output that resembles this: ... PeopleTools version and App Engine details ... FATAL: This is for DEBUG level (0,0) Message Set Number: 0 Message Number: 0 Message Reason: FATAL: This is for DEBUG level (0,0) (0,0) DEBUG: This is for DEBUG level (0,0) Message Set Number: 0 Message Number: 0 Message Reason: DEBUG: This is for DEBUG level (0,0) (0,0) FATAL: This is for FATAL level (0,0) Message Set Number: 0 Message Number: 0 Message Reason: FATAL: This is for FATAL level (0,0) (0,0) Application Engine program APT_LOG_TST ended normally 17.15.34 Application Engine ended normally
The first five lines of our test code create instances of the logging framework application classes. The next block of test code sets the log level to debug and then prints two lines of text. The first line of this block makes use of our fluent interface design decision, making it very clear that we are setting the log level to debug. Unlike the constructor example with its parentheses noise, this fluent interface example is very easy to read. We know this test succeeds because we see two corresponding MessageBox sections in the program’s output. The final block of code tests our isEnabledForXxx methods to see if debug statements print when the log level is set to fatal, which is higher in value than the debug level. Looking at
32
PeopleSoft PeopleTools Tips & Techniques
The Cost of Logging Strings String construction is expensive. When logging, we don’t usually log static strings. Rather, we concatenate static strings with variables that represent the application’s state. Even though the debug and fatal methods test the log level prior to printing, we can further improve performance by using the Logger interface’s isDebugEnabled and isFatalEnabled methods to test the log level prior to concatenating strings. Here is an example: If (&logger.isDebugEnabled()) Then &logger.debug("This is for " | &level.toString() | " level"); End-If;
Executing this code block with the level set to fatal will eliminate the string construction overhead and CPU cycles wasted working through the framework’s method hierarchy.
the output, we see the second DEBUG message is missing, validating our isEnabledForXxx decision logic. Let’s take a closer look at that fluent interface design decision to return a Level from the enableForXxx methods. This is because we use the enableForXxx methods in the context of the setLevel method. If enableForXxx didn’t return a Level object, then this statement: &logger.setLevel(&level.enableForFatal());
would require two lines: &level.enableForFatal(); &logger.setLevel(&level);
More important than line count, the one-line example pairs the purpose of the enable method with the target. It is clear that our intent is to set the logger’s level to fatal. This intent is less clear with the separate statements in the two-line example. The intent is hard enough to discern with the two lines paired. Imagine if &level.enableForFatal() were several lines removed from &logger.setLevel(&level).
Dynamic Logger Configuration The code demonstrated thus far builds the foundation for an extensible logging framework. You can use this foundation as is or extend it by creating additional logging targets and levels. For example, you could create a logger that saves messages in a database table (be sure to use autonomous transactions to keep your log from rolling back with failed transactions). We just have one feature left to develop: the ability to configure loggers at runtime.
The Logger Factory To enable runtime configuration, any code that uses a Logger must be ignorant of the actual Logger implementation. This ignorance represents a level of abstraction12 —the use of a Logger is not associated with any specific instance of a Logger. This abstraction creates a problem:
Chapter 1: Application Classes
33
How do we create instances of an object when we don’t know what object to create? In other words, how do we create a Logger when we don’t know which class provides the Logger implementation? The factory design pattern13 offers a solution to this problem. With the factory design pattern, we will not create Logger instances directly. Rather, we will delegate creation to a factory object. Note This example uses a factory object to encapsulate Logger creation logic. Considering the simplicity of Logger creation, it is possible to use a PeopleCode function library factory function in place of an application class. The requirement “runtime configuration” implies some type of runtime accessible storage implemented through a means as simple as transient global variables or as complex as a persistent, configurable database repository. For this example, we will use a configurable repository. We could implement this repository using one of the various structured file formats, or we could use the PeopleSoft database. Because we have various options for implementing a repository, let’s keep this framework extensible by defining an interface for factories. Implementations of this interface will create and configure loggers from metadata repositories. The following code contains our interface definition: import APT_LOG4PC:Loggers:Logger; interface LoggerFactory method getLogger(&id As string) Returns APT_LOG4PC:Loggers:Logger end-interface;
Developers can usually create tables in the database, but they may not have access to the application server’s file system. Therefore, this example will use a database metadata repository. The record definitions required by this repository are shown in Figure 1-9. Notice the custom field APT_LOGGER_ID. It is defined as a character field with a length of 254 and a format of mixed case. APT_LOGGER_ID is a key field in both records. Note I assume you already know how to build fields and records. If not, then I suggest reading Judi Doolittle’s book PeopleSoft Developer’s Guide for PeopleTools and PeopleCode (McGraw-Hill/Professional, 2008). A logger’s constructor can have an unknown number of parameters. To provide the greatest flexibility in the number and type of constructor parameters, we will use the RECNAME field of APT_LOGGER_CFG to identify the record that holds a logger’s constructor argument values. The record APT_LOGGER_CFG is the main metadata repository table. The APT_FILELOG_CFG record is specific to the FileLogger and contains fields corresponding to the parameters required by the FileLogger constructor. The following listing contains SQL INSERT statements to populate the metadata repository with test data. The test data identifies two loggers for two tests. The first test will log a fatal and a debug message to the MessageBoxLogger, and the second test will log a fatal message to
34
PeopleSoft PeopleTools Tips & Techniques
FIGURE 1-9. Metadata repository
the FileLogger. We will specify the log level for each logger by setting the TRACE_LEVEL field to 100 for debug as well as fatal, and 500 for fatal exclusively. -- Logger 1 INSERT INTO PS_APT_LOGGER_CFG VALUES( 'AE.APT_FACT_TST.MAIN.Step01', 'APT_LOG4PC:Loggers:MessageBoxLogger', 100, ' ') / -- Logger 2 INSERT INTO PS_APT_LOGGER_CFG VALUES ('AE.APT_FACT_TST.MAIN.Step02', 'APT_LOG4PC:Loggers:FileLogger', 500, 'APT_FILELOG_CFG') / -- Constructor parameters for Logger 2
Chapter 1: Application Classes INSERT INTO PS_APT_FILELOG_CFG VALUES ('AE.APT_FACT_TST.MAIN.Step02', 'c:\temp\factory.log') / COMMIT /
The following code listing describes the factory responsible for creating loggers from our database metadata repository: import APT_LOG4PC:Level; import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Factories:LoggerFactory; class DBLoggerFactory implements APT_LOG4PC:Factories:LoggerFactory method getLogger(&id As string) Returns APT_LOG4PC:Loggers:Logger end-class; method getLogger /+ &id as String +/ /+ Returns APT_LOG4PC:Loggers:Logger +/ /+ Extends/implements APT_LOG4PC:Factories:LoggerFactory.getLogger +/ Local array of any &parms = CreateArrayAny(); Local string &loggerClass; Local string &parmRecordName; Local Record &parmRecord; Local number &level; Local number &fieldIdx; SQLExec("SELECT APP_CLASS, TRACE_LEVEL, RECNAME FROM " | "PS_APT_LOGGER_CFG WHERE APT_LOGGER_ID = :1", &id, &loggerClass, &level, &parmRecordName); If (All(&parmRecordName)) Then &parmRecord = CreateRecord(@("RECORD." | &parmRecordName)); &parmRecord.GetField(Field.APT_LOGGER_ID).Value = &id; &parmRecord.SelectByKey(); REM ** Skip first field, logger name; For &fieldIdx = 2 To &parmRecord.FieldCount &parms [&fieldIdx - 1] = &parmRecord.GetField(&fieldIdx).Value; End-For; End-If; Local APT_LOG4PC:Loggers:Logger &logger = CreateObjectArray(&loggerClass, &parms); &logger.setLevel( (create APT_LOG4PC:Level()).enableFromNumber(&level)); Return &logger; end-method;
35
36
PeopleSoft PeopleTools Tips & Techniques
FIGURE 1-10. APT_LOG4PC application package with factories
Besides the create function, PeopleCode has two additional object-creation functions: CreateObject and CreateObjectArray. The getLogger method uses the CreateObjectArray function to create an instance of the correct logger. At design time, if you know the type of object you are creating, then use the strongly typed create function. If you don’t know the class name, but you do know the number of parameters, then use CreateObject. If you don’t know the class name or the number of parameters, then use the CreateObjectArray function. In this example, we do not know the class name or the number of parameters, so we use the most flexible object-creation method: CreateObjectArray. After adding this code to the APT_LOG4PC application package, your final package should resemble Figure 1-10.
Factory Test Program Keeping with our App Engine test theme, create a new App Engine program called APT_FACT_ TST. Add two steps to your App Engine, each with its own PeopleCode action. These steps and actions correspond to the APT_LOGGER_ID values specified in the prior SQL INSERT statement. The next two listings contain the test code for Step01 and Step02, respectively. These listings
Chapter 1: Application Classes differ only by the value assigned to the variable &loggerName. (Don’t forget to disable the Restart property.) Here is the PeopleCode for Step01, which uses logger AE.APT_FACT_TST.MAIN .Step01: import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Factories:DBLoggerFactory; Local string &loggerName = "AE.APT_FACT_TST.MAIN.Step01"; Local APT_LOG4PC:Loggers:Logger &logger = (create APT_LOG4PC:Factories:DBLoggerFactory()) .getLogger(&loggerName); &logger.debug("This message printed from " | &loggerName); &logger.fatal("This message printed from " | &loggerName);
And here is the PeopleCode for Step02, which uses logger AE.APT_FACT_TST.MAIN .Step02: import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Loggers:Logger; import APT_LOG4PC:Factories:DBLoggerFactory; Local string &loggerName = "AE.APT_FACT_TST.MAIN.Step02"; Local APT_LOG4PC:Loggers:Logger &logger = (create APT_LOG4PC:Factories:DBLoggerFactory()) .getLogger(&loggerName); &logger.debug("This message printed from " | &loggerName); &logger.fatal("This message printed from " | &loggerName);
When you run this program, you should see output that resembles the following: ... PeopleTools version and App Engine details ... 21.22.51 .(APT_FACT_TST.MAIN.Step01) (PeopleCode) DEBUG: This message printed from AE.APT_FACT_TST.MAIN.Step01 (0,0) Message Set Number: 0 Message Number: 0 Message Reason: DEBUG: This message printed from AE.APT_FACT_TST.MAIN.Step01 (0,0) (0,0) FATAL: This message printed from AE.APT_FACT_TST.MAIN.Step01 (0,0) Message Set Number: 0 Message Number: 0 Message Reason: FATAL: This message printed from AE.APT_FACT_TST.MAIN.Step01 (0,0) (0,0) 21.22.51 .(APT_FACT_TST.MAIN.Step02) (PeopleCode) Application Engine program APT_FACT_TST ended normally 21.22.51 Application Engine ended normally
37
38
PeopleSoft PeopleTools Tips & Techniques Notice that our App Engine log contains debug and fatal statements from Step01, but no statements from Step02, even though step 1 and 2 use the same code. The logging framework metadata for step 1 described a MessageBoxLogger that prints both debug and fatal messages. The metadata for step 2 described a FileLogger that logs only fatal messages. If you open the file c:\temp\factory.log, you will see only fatal messages: FATAL: This message printed from AE.APT_FACT_TST.MAIN.Step02
Note When run locally through App Designer, this App Engine program will produce the file c:\temp\factory.log in your local C drive. If you run this same code online, the PeopleCode runtime will place the file in your app server’s c:\temp directory. In Chapter 10, you will learn how to use the log4j Java logging framework, which is a robust, extensible logging solution delivered with PeopleSoft applications.
Misuses of Application Classes
As with any technology, developers can—and frequently do—misuse application classes. This section contains examples and scenarios of application class coding errors and inappropriate use cases.
Runtime Context-Sensitive Variables Some system variables exist only when code is running in a specific context. For example, if your code is running inside a component, you can use the %Component system variable. Similarly, if your code is running from a browser request, you can use %Portal, %Request, and %Response. Avoid using these system variables when writing application class PeopleCode. Using context-sensitive system variables violates the reusable principle of application classes. The use of these system variables tightly couples application class code to the runtime execution context. For event-based PeopleCode, this is acceptable, because event-based PeopleCode can run only in the context of the event to which it is attached. Application classes, on the other hand, can run in any execution context. Code that uses %Component online, for example, cannot be called from Integration Broker subscription handlers, App Engine programs, or iScripts. Furthermore, if you tightly couple application classes to their execution context, you cannot effectively apply test-driven development (discussed in Chapter 15).
Indiscriminate Usage Application classes provide a mechanism for creating reusable PeopleCode components. Eventbased PeopleCode, such as component SavePostChange PeopleCode, is not reusable. It is rarely appropriate to delegate event-based processing to application classes. For example, I see programmers try to make application classes into Model-View-Controller (MVC) controllers. MVC is a design pattern used to separate data (the model) from the presentation layer (the view). If you code a component, then you already have a PeopleSoft-managed controller—the component processor.
Chapter 1: Application Classes
39
If you have business logic that you share between online and batch processes, then encapsulating that logic in an application class may make sense. Event-based PeopleCode, however, is usually very component-specific and is not reusable in part, but may be reusable in whole as a component interface. If you share business logic among events within the same component, then the reusable aspect of an application class is compelling. Application classes and function libraries are the only options for sharing this business logic. Whichever form you choose, do your best to make this shared business logic contextually independent of its runtime environment. While your shared business logic may be tightly coupled to your component’s buffer, don’t assume that the code is running in the component processor. Rather, pass a Rowset buffer structure to your shared code as a parameter. Separating your code from its execution context allows you to test your shared business logic offline. You can replicate a buffer structure using nested Rowset structures, but you cannot replicate the component processor’s ability to resolve contextual record.field references.
Conclusion
If you have a strong OOP background, you may be tempted to write all your code in application classes. On the other hand, if you are new to OOP, you might be tempted to avoid application classes altogether. I suggest a middle road approach. Application classes are not a silver bullet. They were not designed to fit every PeopleCode programming scenario. But they are very good at what they do. The remaining chapters will make extensive use of application classes, providing you with several more excellent examples of their use. You will also find several excellent examples of applications classes among the PeopleSoftdelivered application PeopleCode. As you will see in Chapter 3, the Approval Workflow Engine is an outstanding framework built using application classes. Likewise, the application package EOIU:Common contains several reusable classes for working with collections. I encourage you to investigate the delivered PeopleCode application classes. You will find valuable, reusable application classes and code snippets you can use to improve your own business logic.
Notes 1. Ssdlpedia, “Spartan programming” [online]; available from http://ssdl-wiki.cs.technion .ac.il/wiki/index.php/Spartan_programming; Internet; accessed 18 February 2009. 2. Jeff Atwood, Coding Horror, “Spartan Programming” [online]; available from http://www .codinghorror.com/blog/archives/001148.html; Internet; accessed 18 February 2009. 3. Paul E. Davis, “Code Reduction or Spartan Programming” [online]; available from http:// willcode4beer.com/design.jsp?set=codeReduction; Internet; accessed 18 February 2009. 4. BrainyQuote, Albert Einstein Quotes [online]; available from http://www.brainyquote.com/ quotes/quotes/a/alberteins109011.html; Internet; accessed 18 February 2009. 5. Oracle Corporation, Sun Developer Network, Code Conventions for the Java Programming Language [online]; available from http://java.sun.com/docs/codeconv/; Internet; accessed 17 February 2009. 6. Wikipedia, “Mutator method” [online]; available from http://en.wikipedia.org/wiki/ Mutator_method; Internet; accessed 18 October 2009.
40
PeopleSoft PeopleTools Tips & Techniques 7. Oracle Corporation, Enterprise PeopleTools 8.49 PeopleBook: PeopleCode API Reference, Application Classes, “Differentiating Between Properties and Methods” [online]; available from http://download.oracle.com/docs/cd/E13292_01/pt849pbr0/eng/psbooks/tpcr/book .htm?File=tpcr/htm/tpcr07.htm#d0e17547; Internet; accessed 22 February 2009. 8. Martin Fowler, “FluentInterface” [online]; available from http://www.martinfowler.com/ bliki/FluentInterface.html; Internet; accessed 22 February 2009. 9. Martin Fowler, “Method Chaining” [online]; available from http://martinfowler.com/ dslwip/MethodChaining.html; Internet; accessed 22 February 2009. 10. Kioskia.net, “OOP–Data encapsulation” [online]; available from http://en.kioskea.net/ contents/poo/encapsul.php3; Internet; accessed 22 February 2009. 11. Oracle Corporation, Enterprise PeopleTools 8.49 PeopleBook: PeopleCode API Reference, Application Classes, “Constructors” [online]; available from http://download .oracle.com/docs/cd/E13292_01/pt849pbr0/eng/psbooks/tpcr/book.htm?File=tpcr/htm/ tpcr07.htm#d0e17124; Internet; accessed 22 February 2009. 12. TheFreeDictionary, “abstraction” [online]; available from http://www.thefreedictionary .com/abstraction; Internet; accessed 21 May 2009. 13. Wikipedia, “Factory method pattern” [online]; available from: http://en.wikipedia.org/ wiki/Factory_method_pattern; Internet; accessed 19 October 2009.
Chapter
2
The File Attachment API
42
PeopleSoft PeopleTools Tips & Techniques
A
pplication users often need to view and store documents related to transactions. For example, an internal auditor needs access to receipts and invoices to validate expense reports and vouchers. The ability to route and view receipts electronically, within the context of the related transaction, improves efficiency in transaction processing.
In addition to attachments, the File Attachment API offers a mechanism for users to transfer files from their desktops to the app server for processing. Consider file-based integrations, which often originate with a user. For example, a training coordinator may use a spreadsheet program to generate a training plan and class schedule for the next quarter. The method used to copy data from that spreadsheet into PeopleSoft’s Enterprise Learning Management module can have a significant impact on the training coordinator’s PeopleSoft experience. For users, the File Attachment API provides an experience similar to attaching files to an e-mail message. The difference is the target is a transaction, not an e-mail message. In this chapter, you will create a new transaction, add file attachment functionality to that transaction, and modify an existing transaction. Along the way, you will learn tips for minimizing the impact of user interface modifications.
Adding Attachments to Transactions Adding a file attachment requires the following steps:
1. Investigate the target transaction. Identify pages and components. 2. Create a storage record for storing attachments. 3. Add the FILE_ATTACH_SBR subrecord to either a transaction record or a custom, related record. 4. Add attachment buttons to a page (Add, Delete, and View). 5. Write PeopleCode for the attachment buttons. In Part II of this book, we will enhance the user experience through rich Internet technologies. Some of those technologies, such as Flash and JavaFX, require binary files. We need a way to get files from our desktop to the server, where they can enhance the user experience. We will call these binary files web assets. In this chapter, we will build a page and a component for developers to upload and describe web assets. The metadata describing these web assets will compose our transaction. We will use the File Attachment API to upload files from our desktop and save them with the transaction.
Investigating the Target Transaction Since we will be building a new transaction, our investigation will focus on identifying the transaction’s requirements. We will then build data and user interface elements to support that transaction. Each web asset will need an ID field. We will also want a description field that communicates the purpose for the web asset. In Part II, when we write code to serve these web assets to the user’s browser, we will need to tell the browser the content type for the binary file. Let’s also add a comment field so we can add notes about the asset, annotating the change history or background information of the web asset.
Chapter 2: The File Attachment API
43
FIGURE 2-1. The APT_WEB_ASSETS record definition
With the data elements identified, we can now create the transaction’s record definition. Figure 2-1 shows the record that will contain web asset metadata. Notice that ASSET_ID is a primary key field. The DESCR and MIMETYPE fields are alternate search keys. Each of these three fields is selected as a list box item. Save and build the record. When prompted for a record name, enter APT_WEB_ASSETS. Our next step is to create a data-entry page. Figure 2-2 shows this page, named APT_WEB_ ASSETS. Because the MIMETYPE field is 100 characters long, the MIMETYPE field text box that App Designer creates is significantly longer than the space available on the page. To change the size of this field, double-click the text box and select Custom from the Size group box. Also notice that ASSET_ID is set to display only. Since ASSET_ID is our primary key, users cannot edit this field’s value. The value for ASSET_ID is set when a user adds a new value from the component search page.
44
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-2. The APT_WEB_ASSETS transaction page
Let’s build the APT_WEB_ASSETS component next, as shown in Figure 2-3. After dropping the APT_WEB_ASSETS page on a new component, set the component’s search record to APT_ WEB_ASSETS and change the page’s item label to Web Assets. We are almost ready to register this component. The Registration Wizard requires the following information: ■■ Menu ■■ Folder ■■ Permission list We need to identify these items before continuing. We will identify the menu and permission list by creating a new menu and a new permission list. The answer to the folder question resides in the portal registry. Since we don’t need to create any objects to answer the folder question, let’s answer it now. Log in to PeopleSoft using your web browser and navigate to PeopleTools | Portal | Structure and Content.
Chapter 2: The File Attachment API
45
FIGURE 2-3. The APT_WEB_ASSETS component
Within the portal registry, navigate to PeopleTools | Utilities. Select the Edit link next to the Administration folder reference. This will allow you to view the name of this folder, as shown in Figure 2-4. Now we need to create the menu. Figure 2-5 is a screenshot of the empty menu, which is named CUSTOM. Unless otherwise noted, the examples in this book will use this menu when registering custom components. Note Before you can save a new menu definition, App Designer requires you to add one item to the menu. You can delete the item later, or even ignore it indefinitely. Figure 2-5 shows the CUSTOM menu with a separator item. As with all PeopleSoft pages, if you want to view this page online, you need to add it to a permission list. Many examples in this book will require security. Let’s create a custom permission
46
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-4. Administration folder details
list and role for use with the rest of the examples in this book. Figure 2-6 shows the custom permission list APT_CUSTOM, and Figure 2-7 shows the corresponding role APT_CUSTOM. Now that all registration prerequisites are complete, we can register the component. With the APT_WEB_ASSETS component open in App Designer, choose Tools | Register Component. Step through the Registration Wizard as you would when registering any component. 1. On the Start page, choose the following: ■■ Add this component to a menu. ■■ Add this component to a portal registry. ■■ Add this component to a permission list. 2. On the Add to a Menu and Bar page, select the menu APT_CUSTOM and the bar CUSTOM.
Chapter 2: The File Attachment API
47
FIGURE 2-5. APT_CUSTOM menu 3. On the Create Content Reference page, make these selections: ■■ Ensure the Target Content radio button is selected. ■■ Select the PT_ADMINISTRATION folder. ■■ Change the content reference label to Web Assets. ■■ Change the long description to Component for maintaining web user interface assets. ■■ Select the template name of DEFAULT_TEMPLATE. ■■ Select the appropriate node for your application. (Since I am using an HRMS application, I selected the node named HRMS, but your node name may differ.) 4. On the Add to Permission List page, select the APT_CUSTOM permission list. 5. On the Finish page, select Menu, Registry Entry, and Permission List to add these items to your project.
48
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-6. APT_CUSTOM permission list
The Registration Wizard sets the label on the content reference, but not the label on the menu. To set the menu label, open the menu APT_CUSTOM and expand the CUSTOM menu item. Double-click APT_WEB_ASSETS to display the properties for this menu item, and then set the label to Web Assets, as shown in Figure 2-8. Save the menu. Tip When migrating projects that contain content references, add the parent folder for the content reference to the project. For example, if you migrate this project, add the PT_ADMINISTRATION folder to your project. Prior to migrating your project, set the upgrade action to Copy and enable the Upgrade check box, even if the parent folder is unchanged. This will cause PeopleTools to recache the contents of the parent folder, making your new content reference available immediately.
Chapter 2: The File Attachment API
49
FIGURE 2-7. APT_CUSTOM role
Let’s test the transaction. First, log in to your PeopleSoft application using your web browser and add the APT_CUSTOM role to your user profile. After the role is in your user profile, navigate to PeopleTools | Utilities | Administration. You should see a new item labeled Web Assets. Select this menu item and add the new value TEST, as shown in Figure 2-9.
Creating the Attachment Storage Record Now that we have a working transaction, it is time to add attachment functionality. PeopleSoft offers two attachment storage methods: FTP and record. Both storage methods use HTTP to transfer the file from the client browser to the web server. After the PeopleSoft application receives the file from the user’s web browser, it will either forward the attachment to an FTP server or write the attachment to a database record. The choice between FTP and database record is a storage choice, not a transfer choice. Normally, we think of FTP as a method for transferring files, and therefore, may think that an FTP attachment is transferred from the client to the server using FTP, but it isn’t.
50
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-8. Web Assets menu item
The storage type you choose will differ depending on your attachment purpose, system configuration, and business needs. The File Attachment API functions provide access to attached files for both storage types. One difference between the record and FTP storage options is that the record option actually allows you to read and process a binary file directly from PeopleCode. A record field can contain binary data, and that binary data can be assigned to a PeopleCode variable. However, a binary file cannot be read into a PeopleCode variable, because PeopleCode does not offer functions for reading binary data from binary files. For this transaction, we will store attachments in a database record. Once you know how to use record storage, then FTP storage is trivial. The page-design considerations for both methods are the same. There are many differences between FTP and record storage from a user and administration perspective. The only difference between the two methods from a developer’s perspective is that database record storage requires you to build a new record definition.
Chapter 2: The File Attachment API
FIGURE 2-9. Adding the TEST web asset
FTP or Record Storage—Which Is Better? PeopleSoft offers FTP or record-based storage solutions for file attachments. While I prefer record-based storage, each has its place. The following are some considerations when choosing between the two methods: ■■ Cloning and backup Since attached files exist within a database record, cloning is a simple matter of copying the database. There are no additional transactionreferenced file system files to copy as there are with FTP storage. Similarly, a standard database backup routine will maintain backup copies of critical transaction-related documents. When storing files in an FTP storage location, you must consider external file backup strategies. ■■ Point-in-time recovery When storing files within the database, your relational database management system (RDBMS) transaction logs maintain rollback and recovery information that identifies file attachments as members of the same database transaction as the online component’s transaction. FTP storage does not offer this same capability.
51
52
PeopleSoft PeopleTools Tips & Techniques
■■ Canceled transactions Another issue with FTP storage that is resolved by standard RDBMS transaction management is dealing with canceled transactions (the user canceled a transaction or, even worse, a transaction was canceled because of an error). When storing attachments in database records, the standard RDBMS rollback will eliminate orphaned attachment data. With FTP storage, if a user cancels a transaction after uploading a file, the attachment file will persist. ■■ Accessibility One of the key benefits of FTP storage over database record storage is accessibility. Users can readily access files residing on an FTP server using standard file browsing, viewing, and administration tools. Through network shares, it is possible for users to view FTP stored files without logging in to PeopleSoft.
To store attachments in the database, we need to create a record definition. Create a new record as you did for the transaction record, but don’t add any fields. From the Insert menu, choose Subrecord and insert the FILE_ATTDET_SBR subrecord into this new record, as shown in Figure 2-10. Save the record as APT_WA_ATTDET and build it.
FIGURE 2-10. APT_WA_ATTDET record definition
Chapter 2: The File Attachment API
53
Because PeopleTools limits the length of record names to 15 characters, record names can be quite cryptic. Allow me to decrypt this new record’s name. All custom objects in this book’s examples are prefixed with APT. Therefore, the APT in this record’s name represents our custom object prefix. The WA in the middle of the name is an abbreviation for Web Assets, and the ATTDET is an abbreviation for ATTachment DETails. This new record, APT_WA_ATTDET, will contain our attachment file data. Let’s explore the fields in this record: ■■ ATTACHSYSFILENAME represents the unique identifier for an attachment. The unique identifier is the original filename with certain characters, such as spaces, replaced with underscores. If we choose, we can prefix the unique identifier with the transaction’s keys. ■■ If the presence of the FILE_SEQ field makes you think of chunked data, you are correct. Attachments stored in database records are chunked according to chunking rules in the PeopleTools Options page. The Max Chunk File Size field on the PeopleTools Options page determines the size of each chunk. If a file is larger than the Max Chunk File Size value, then the File Attachment API will insert multiple rows into APT_WA_ATTDET— one row for each chunk. ■■ The FILE_SIZE field contains the attachment file’s length. If a file is larger than the Max Chunk File Size value specified in the PeopleTools Options page, this field will contain the size of the chunk stored in a given row. The total size of the attached file is the sum of all the FILE_SIZE values for a given ATTACHSYSFILENAME. ■■ The file’s actual data is stored in the FILE_DATA field.
Adding the FILE_ATTACH_SBR Subrecord Notice that this record definition does not contain any fields to associate the attachment with a transaction. Rather than add the transaction record’s keys to the attachment record, we must add the attachment record’s keys to the transaction record. This allows the File Attachment API to keep the attachment functions generic enough for use with any transaction. The way we relate a transaction record to an attachment record is through a subrecord called FILE_ATTACH_SBR. The FILE_ATTACH_SBR subrecord contains the ATTACHSYSFILENAME key field you saw in the attachment record. Since the transaction record is a custom record we created, we could add this subrecord to the main transaction record, APT_WEB_ASSET. However, since I’m sure you are reading this chapter because you want to learn how to add attachments to delivered transactions, let’s take an alternative approach. Following best practices, we don’t want to modify delivered record definitions. Rather, we can create a secondary record that has the same keys as the transaction, plus the FILE_ATTACH_SBR. This allows us to associate transactions with attachments without modifying delivered records. Figure 2-11 shows this new transaction/attachment record named APT_WA_ATTACH. Note that ASSET_ID is a key field.
Adding Attachment Fields and Buttons to the Transaction Page With the records defined, we can modify the transaction to include attachment fields. Start by adding a new edit box to the APT_WEB_ASSETS page. Set the field’s source record to APT_WA_ ATTACH and source field to ATTACHUSERFILE. This is the attachment file display name—the name chosen by the user. Displaying the attachment name is a nice gesture if the transaction contains an attachment.
54
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-11. APT_WA_ATTACH transaction/attachment record
We need to modify a few settings for this field. It is too large for our page, and we don’t want users typing values into this field. Rather, we want the user to use the designated attachment buttons to add or remove attachments. To change these settings, open the page field’s Properties dialog and set the Size to custom. On the Use tab, enable the Display Only check box. After closing the Properties dialog by clicking OK, resize the field to fit on the page. In a like manner, add the ATTACHSYSFILENAME field to the page. We are adding this field to the page just to make it available to PeopleCode. We do not want to display this field to users. Therefore, on the Use tab, mark this field as Invisible. Figure 2-12 shows this modified page within App Designer. Notice that the Sys Filename field appears far off to the right. The display location of this field is irrelevant since it is a hidden field. Tip Placing hidden fields in odd or irregular locations is a good practice. When you open a page with fields in odd places, your eye is drawn to those fields. If hidden fields are placed in standard locations, they blend in with regular fields and are harder to identify.
Chapter 2: The File Attachment API
55
FIGURE 2-12. APT_WEB_ASSETS page with attachment fields
Test the page in your browser to make sure it works as expected. When you navigate to the page, PeopleTools | Utilities | Administration | Web Assets, you should see the test transaction we previously created. The new attachment field should be visible, but it will not have a value. We now have a page and record for adding and storing attachments, but we are missing the PeopleCode and buttons to make it functional. Let’s add some buttons to this page to allow users to add, view, and remove, attachments. Adding buttons to a page typically requires creating a derived/work record with fields designated as push buttons. To simplify page development, PeopleTools provides the FILE_ATTACH_WRK record with add, view, delete, and detach button fields. Add the ATTACHADD, ATTACHVIEW, and ATTACHDELETE buttons to the APT_WEB_ ASSETS page. Figure 2-13 shows this transaction page in App Designer with the buttons added. Notice that I added a group box to the bottom of the page and placed the buttons and ATTACHUSERFILE field inside this group box.
56
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-13. APT_WEB_ASSETS page with attachment buttons
Figures 2-14 and 2-15 show the properties for the Add button. The properties for the View and Delete buttons are the same, except for the field names. For the View button, choose the field name ATTACHVIEW. For the Delete button, choose the field name ATTACHDELETE. Reload the page in your browser just to make sure the buttons display properly, as shown in Figure 2-16. We haven’t attached any code to the buttons so, at this point, they are still not functional. When storing attachments using the File Attachment API, you specify the storage location using a URL. For FTP storage, a URL contains all of the information required to log in to an FTP server and save a file. Here is an example of an FTP URL that includes a username and password: ftp://user:password@servername/folder/for/uploaded/files For record-based storage, PeopleSoft uses a custom URL specification that looks like this: record://recordname where recordname represents the name of a database record definition.
Chapter 2: The File Attachment API
57
FIGURE 2-14. ATTACHADD button type properties
A URL is composed of a protocol and a resource. The PeopleSoft record URL protocol is record, and the resource is recordname. In our case, the URL will be record://APT_WA_ATTDET. Note Be sure to update your FTP file attachment URLs when migrating code or cloning databases. I recommend storing FTP URLs as URL definitions and adding SQL to your clone scripts to update URL definition values. As a precaution, I also recommend eliminating network routes between your production system and your development, test, quality assurance, and other PeopleSoft systems. In the event that a URL in one of these other systems continues to point to production, eliminating network routes (DNS, proxy, firewall, and so on) will keep your nonproduction systems from affecting your production systems.
58
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-15. ATTACHADD button label properties
When we attach code to the Add, View, and Delete buttons, we will need to specify the attachment repository’s URL. In that code, we can either hard-code the URL as a string literal or store the URL in one of PeopleSoft’s configurable string repositories. In Chapter 1, you saw how we could use the message catalog to store strings and retrieve them with MsgGetText. PeopleSoft provides another string repository specifically designed for URLs and a corresponding function named GetURL. One of the benefits of using the URL catalog is that it allows administrators to maintain URL values without modifying code. We will use this URL catalog to store the URL record://APT_WA_ATTDET. To add a new URL, log in to your PeopleSoft web application and navigate to PeopleTools | Utilities | Administration | URLs. Add a new value named APT_WEB_ASSET_ATTACHMENTS, as shown in Figure 2-17. After creating the URL definition, you can add it to your project and migrate it between environments, just like other PeopleTools-managed definition.
Chapter 2: The File Attachment API
59
FIGURE 2-16. Web Assets page online view with attachment buttons
Writing PeopleCode for the Attachment Buttons With the user interface elements complete, it is time to make the buttons functional. When adding PeopleCode to buttons, we have a couple of options: ■■ Add PeopleCode to record events. ■■ Add PeopleCode to component record events. Generally speaking, where you place your code (record or component) depends on whether that code should execute every time a button is placed on a page or only for certain components. In this case, our code is specific to the APT_WEB_ASSETS component, and therefore, it should be placed at the component level rather than the record level. Furthermore, our buttons are bound to a delivered record, and we don’t want to modify delivered definitions if we can avoid doing so. We will start with the Add button.
60
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-17. APT_WEB_ASSET_ATTACHMENTS URL definition
The Add Button Open the component APT_WEB_ASSETS. From the View menu, select View PeopleCode. In the first drop-down list, select the ATTACHADD field. In the second drop-down list, select FieldChange. Figure 2-18 shows the APT_WEB_ASSETS.GBL.FILE_ATTACH_WRK.ATTACHADD.FieldChange code editor prior to adding code. Caution Each of the following PeopleCode events refers to the APT_WEB_ ASSETS component PeopleCode, not record PeopleCode. Even though the event-based PeopleCode is attached to the FILE_ ATTACH_WRK record and fields, be sure to add the following PeopleCode to the component, not the record definition. Adding this PeopleCode directly to FILE_ATTACH_WRK may cause other attachments to fail, because the FILE_ATTACH_WRK record is shared by various transactions.
Chapter 2: The File Attachment API
FIGURE 2-18. ATTACHADD FieldChange event PeopleCode editor Add the following code to this editor: Declare Function add_attachment PeopleCode FILE_ATTACH_WRK.ATTACHADD FieldChange; Declare Function display_attachment_buttons PeopleCode FILE_ATTACH_WRK.ATTACHADD RowInit; Local string &recname = "Record." | Record.APT_WEB_ASSETS; Local number &retcode; add_attachment(URL.APT_WEB_ASSET_ATTACHMENTS, "", "", 0, True, &recname, APT_WA_ATTACH.ATTACHSYSFILENAME, APT_WA_ATTACH.ATTACHUSERFILE, 2, &retcode); If (&retcode = %Attachment_Success) Then display_attachment_buttons(APT_WA_ATTACH.ATTACHUSERFILE); End-If;
61
62
PeopleSoft PeopleTools Tips & Techniques Note Be sure to choose the correct event when adding PeopleCode. When you open the PeopleCode editor for a field, the editor initially displays the FieldDefault event. In this scenario, we want to add code to the FieldChange event. Make sure you change the event prior to adding your code. Let’s walk through this code line by line. The first two lines declare attachment helper functions: Declare Function add_attachment PeopleCode FILE_ATTACH_WRK.ATTACHADD FieldChange; Declare Function display_attachment_buttons PeopleCode FILE_ATTACH_WRK.ATTACHADD RowInit;
The add_attachment function is a PeopleCode FUNCLIB wrapper around the native AddAttachment PeopleCode function. Besides calling AddAttachment, the add_ attachment wrapper function performs the following operations: ■■ Adds the transaction record’s primary key values to ATTACHSYSFILENAME, the value that represents the attachment data’s primary key. This ensures the primary key for the attachment data is unique. ■■ Sets the values of the transaction record’s ATTACHUSERFILE and ATTACHSYSFILENAME field values to the values chosen by the user and the values stored in the attachment data table. ■■ Displays an error message if an error occurs. The third line declares a variable with the value Record.APT_WEB_ASSETS. Local string &recname = "Record." | Record.APT_WEB_ASSETS;
Notice that we use concatenation and a PeopleCode definition reference to build this string. If we quoted the name of the record rather than using a definition reference, then the Find Definition References feature would fail to find this usage of the record definition. Even though the syntax looks more cluttered than the string representation, the maintenance impact of this decision makes the extra syntax worth the effort. We will pass this string into the add_attachment function to tell the function where to find the transaction’s key fields. Line 4 declares a variable to receive the AddAttachment return code: Local number &retcode;
The add_attachment wrapper function will set this variable to the return value produced by the native AddAttachment function. Line 5 calls the add_attachment function, passing in the URL definition, the transaction record, the filename fields, and the return code variable: add_attachment(URL.APT_WEB_ASSET_ATTACHMENTS, "", "", 0, True, &recname, APT_WA_ATTACH.ATTACHSYSFILENAME, APT_WA_ATTACH.ATTACHUSERFILE, 2, &retcode);
Chapter 2: The File Attachment API
63
The add_attachment declaration follows: add_attachment(&URL_ID, &FILEEXTENSION, &SUBDIRECTORY, &FILESIZE, &PREPEND, &RECNAME, &ATTACHSYSFILENAME, &ATTACHUSERFILE, &MESSAGE_LVL, &RETCODE);
The add_attachment PeopleCode in FILE_ATTACH_WRK.ATTACHADD.FieldChange contains comments describing each of the function’s parameters, many of which come from the AddAttachment native PeopleCode function. You can find additional information about the AddAttachment function parameters in the PeopleBooks PeopleCode Language Reference. The first parameter, URL_ID, can be either a string URL or the ID of a URL definition. As you can see, we use the ID of a URL definition. String URLs can be FTP or RECORD URLs of this form: ftp://user:password@server/directory or RECORD://recordname We pass empty strings for the file extension and subdirectory parameters. The file extension parameter is not used by modern web browsers. The subdirectory parameter is used only for FTP storage repositories. The next parameter, identified by the number 0 (zero), represents the maximum file size for an attachment in kilobytes. By specifying 0, we are telling the AddAttachment function to allow files of any size. If you prefer to restrict the maximum allowable file size, then set this to a different value. Because the HTML standard file input button does not provide a mechanism for limiting files by size, the size of the file is not validated until after the file reaches the web server. Note If your purpose for limiting a file’s size is to reduce bandwidth usage between your client browsers and the web server, then the delivered maximum file size parameter will not satisfy your needs. Nevertheless, it is possible to perform client-side file size validation. A friend of mine uses the Microsoft Scripting.FileSystemObject script runtime object to restrict uploads by file size prior to submission. Unfortunately, this solution works only with Internet Explorer. By passing the Boolean value True and the variable &recname, we are telling the add_ attachment function to prefix the ATTACHSYSFILENAME value with the keys from the record &recname. In this example, &recname represents APT_WEB_ASSETS. This ensures the uniqueness of filenames added to APT_WA_ATTDET, our file attachment storage record. Note Depending on the key structure of your component and the length of the uploaded filename, it is possible that a concatenated filename will exceed the maximum length of 128 characters. The add_attachment PeopleCode function contains code to limit the concatenated keys to a length of 64 characters, leaving 64 characters for the original uploaded filename. The next two parameters, APT_WA_ATTACH.ATTACHSYSFILENAME and APT_WA_ATTACH .ATTACHUSERFILE, represent the relationship between the transaction and the attachment storage location. APT_WA_ATTACH.ATTACHSYSFILENAME is the key value used to map
64
PeopleSoft PeopleTools Tips & Techniques between the storage location and the transaction. APT_WA_ATTACH.ATTACHUSERFILE is the original filename and will be used to display the filename to the user. &MESSAGE_LVL is an interesting parameter. This parameter tells add_attachment how to handle error notifications. When add_attachment calls AddAttachment, if the return code is an error code, should add_attachment display the error message? A value of 0 suppresses error messages. A value of 1 means display all messages including a success message. We specified a value of 2, which means to display error messages only. If you prefer, you can specify a value of 0, for no messages, and then evaluate the &RETCODE parameter to determine if the function was successful. The last three lines of code in this FieldChange event enable and disable buttons based on the state of the attachment: If (&retcode = %Attachment_Success) Then display_attachment_buttons(APT_WA_ATTACH.ATTACHUSERFILE); End-If;
If the add_attachment function succeeded, then call the display_attachment_buttons FUNCLIB function. The display_attachment_buttons function hides the Add button and shows the View and Delete buttons if the attachment field APT_WA_ATTACH.ATTACHUSERFILE has a value. Otherwise, it hides the View and Delete buttons and shows the Add button.
The Delete Button Next, add the following code to the ATTACHDELETE FieldChange event. This code is very similar to the ATTACHADD PeopleCode. The delete function needs to know the attachment storage location, the attachment storage key values, and how to handle error messages. Declare Function delete_attachment PeopleCode FILE_ATTACH_WRK.ATTACHDELETE FieldChange; Declare Function display_attachment_buttons PeopleCode FILE_ATTACH_WRK.ATTACHADD RowInit; Local number &retcode; delete_attachment(URL.APT_WEB_ASSET_ATTACHMENTS, APT_WA_ATTACH.ATTACHSYSFILENAME, APT_WA_ATTACH.ATTACHUSERFILE, 2, &retcode); If (&retcode = %Attachment_Success) Then display_attachment_buttons(APT_WA_ATTACH.ATTACHUSERFILE); End-If;
The View Button Add the following code to the ATTACHVIEW FieldChange event. This will allow users to download (view) attachments by clicking the View button. Declare Function view_attachment PeopleCode FILE_ATTACH_WRK.ATTACHVIEW FieldChange;
Chapter 2: The File Attachment API
65
Local number &retcode; view_attachment(URL.APT_WEB_ASSET_ATTACHMENTS, APT_WA_ATTACH.ATTACHSYSFILENAME, APT_WA_ATTACH.ATTACHUSERFILE, 2, &retcode);
Noticing a pattern? The view_attachment FUNCLIB function also needs to know the storage location, the attachment key value, and how to handle messages.
Initializing Buttons The PeopleCode for the Add and Delete buttons hides and shows buttons based on the current state of the transaction’s attachment. What about when the page is initially displayed? To hide the Delete and View buttons on new transactions or to show them on existing transactions, add the following PeopleCode to the component’s FILE_ATTACH_WRK RowInit event. This is the same code we used previously to set the state of our buttons from the Add and Delete FieldChange events. Declare Function display_attachment_buttons PeopleCode FILE_ATTACH_WRK.ATTACHADD RowInit; display_attachment_buttons(APT_WA_ATTACH.ATTACHUSERFILE);
Testing the Buttons Using your web browser, navigate to the Web Assets page. Do you see an Add button, but no View or Delete buttons? If so, then your RowInit event is working correctly. Use the Add button to add an attachment. Pick any small file from your computer. If you can’t find a small file, open Notepad and create text file that contains only the text “Hello World.” After uploading a file, you should see the Add button disappear and the View and Delete buttons appear. If you click the View button, your browser should respond by opening that small file in a new browser window. If the file you chose was a type that the browser can interpret, its contents will be shown in the browser window. If the file was a binary file that the browser cannot interpret, such as an .exe file, the browser will prompt you to open or download the file. Tip The ViewAttachment function uses JavaScript to open a new window. Modern browsers and browser plug-ins block this behavior, assuming any page that uses JavaScript to display a new window is malicious at worst and annoying at best. If you have a pop-up blocker, or just a modern browser, then you may need to enable popups for your PeopleSoft URL so the ViewAttachment function will work properly.
Customizing File Attachment Behavior The delivered file attachment function library simplifies development by hiding some of the File Attachment API’s less common features. In this section we will write our own function library that maintains most of the simplicity of the delivered library while exposing these additional features.
66
PeopleSoft PeopleTools Tips & Techniques Through writing this custom library, you will learn how to interact directly with the low level File Attachment API.
Button State Using the delivered display_attachment_buttons function, when the transaction has an attachment, the Add button is invisible, and the View and Delete buttons are visible. Conversely, when the transaction has no attachment, the Add button is visible and the View and Delete buttons are hidden. While I certainly advocate disabling features based on the state of a transaction, I am not fond of hiding features users are accustom to seeing. As an alternative, it would be better to enable and disable these buttons based on the state of the transaction. The delivered display_attachment_buttons FUNCLIB function also assumes the attachment buttons are bound to the FILE_ATTACH_WRK derived/work record. It would be nice to have a more generic version that allows us to bind these buttons to any record. To make these changes, we will create our own function named APT_set_buttons_state in a new FUNCLIB. Before creating the function, we need a new FUNCLIB. A FUNCLIB is a reusable function stored in a derived/work record field event. So, first create a new record and set the record type to Derived/Work. FUNCLIB records do not hold data, so they do not need to exist at the database level. Add the field ATTACHADD to this record and save the record as APT_ATTACH_FUNC. (The name of the record is not important.) Your record should look something like Figure 2-19.
FIGURE 2-19. APT_ATTACH_FUNC record definition
Chapter 2: The File Attachment API
67
Note PeopleSoft recommends that customers suffix FUNCLIB record names with _FUNCLIB. This helps differentiate FUNCLIB records from work records and database records. Because record names are limited to 15 characters, and the first 4 are consumed by our prefix, it is a shame to waste the final 8 on the suffix _FUNCLIB. That would leave us with only 3 characters to uniquely identify our FUNCLIB. Rather than creating an unintelligible acronym, I chose to shorten the recommended suffix to just _FUNC. PeopleSoft recommends placing FUNCLIB PeopleCode in the FieldFormula event, so add the following code to the FieldFormula event of the ATTACHADD field: /* * Function: APT_set_buttons_state * * &filename string Generally ATTACHUSERFILENAME field * &btn_rec record The record bound to the attachment buttons * This is generally FILE_ATTACH_WRK and has * the fields ATTACHADD, ATTACHVIEW, * ATTACHDELETE, and ATTACHDET. */ Function APT_set_buttons_state(&attachuserfilename As string, &btn_rec As Record) Local boolean &add = None(&attachuserfilename); REM ** array of possible button names; Local array of string &fields = Split("ATTACHVIEW ATTACHDELETE ATTACHDET"); local string &field_ref; Local number &field_idx = 0; REM ** Assume ATTACHADD is the name of the ADD button; &btn_rec.ATTACHADD.Enabled = &add; While &fields.Next(&field_idx) REM ** try block just in case field doesn't exist; try &field_ref = "Field." | &fields [&field_idx]; &btn_rec.GetField(@(&field_ref)).Enabled = ( Not &add); catch Exception &e1 REM ** Field is not in record -- ignore; end-try; End-While End-Function;
This function introduces some tricks for creating generic routines. The first trick is that we initialize and populate an array of field names in one line by using the PeopleCode Split function. We then iterate over that array, checking to see if the field exists in the record. Since all
68
PeopleSoft PeopleTools Tips & Techniques buttons get the opposite state of the Add button, we set the button’s state to Not &add. The try / catch / end-try construct ignores fields that don’t exist in &btn_rec. This allows developers to create custom button records without the Delete, View, or Detach buttons. For example, I wrote a batch program once that processed a file. That file came from a file upload on the run control page. I found that users would reuse the same run control, replacing the file with each run. If the process failed, I would not have the file as an audit trail to help me understand why the process failed. To resolve this, I removed the Delete button, requiring users to create new run controls for new files. We can now replace the display_attachment_buttons function in each of the three events (Add FieldChange, Delete FieldChange, and RowInit) with the following declaration and function call: REM ** replace declaration with; Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; REM ** replace function call with; APT_set_buttons_state(APT_WA_ATTACH.ATTACHUSERFILE, GetRecord(Record.FILE_ATTACH_WRK));
Custom Add Attachment Function Figure 2-20 shows the delivered File Attachment API file upload page. While the Browse and Upload buttons, in the context of a transaction, may be enough to tell the user what to do, it would be nice to add a little more to this page to provide users with instructions. The native AddAttachment PeopleCode function actually allows you to add HTML to this page. Let’s create our own version of the add_attachment function that takes an HTML parameter. The current implementation also assumes that the primary key record is available from the current context using GetRecord(). This will not work when inserting attachments into a scroll area at a level lower than the row containing the button. Also, if the component contains the same record at multiple levels, then GetRecord() will return the instance that is closest to the button’s execution context, which may not be the appropriate instance. Furthermore, while it could be argued that the AddAttachment function is relevant only within a component context, I prefer to write FUNCLIB functions to be independent of their runtime context. With this in mind, let’s write our own version of the add_attachment function. Add the following PeopleCode to the FieldFormula event of the APT_ATTACH_FUNC.ATTACHADD record field. /* * Function: APT_add_attachment * * Parameters: * &url string Either a URL ID or a string in the * format: * ftp://user:password@server/ * or * record://recname * &max_filesize number Maximum size of attachment * &prefix_keys boolean Add transaction keys to &sys_filename * &key_rec record Record containing keys
Chapter 2: The File Attachment API
FIGURE 2-20. File Attachment API upload page * * * * * * * * * * * * * * * * * * *
&sys_dir &sys_filename
string string
&user_filename
string
&preserve_case
boolean
&html_header
string
&handle_errors
boolean
FTP server sub directory On input, a prefix for files that will be stored in the ftp or record storage location. On return, the name of the file in the storage location The name of the file uploaded by the user. populated on return. Output parameter only. True to maintain the case of the file name received from the user HTML to display above the file input text box on the attachment page. If this value is empty, then this function will use the HTML from the HTML definition APT_FILE_ATTACH_HEADER True to display errors, false to suppress errors. If you choose false, you should inspect the return value.
69
70
PeopleSoft PeopleTools Tips & Techniques * Returns: return value of AddAttachment or -1 if &prefix_keys is * true and user hasn't populated all the key fields. * */ Function APT_add_attachment(&url As string, &max_filesize As number, &prefix_keys As boolean, &key_rec As Record, &sys_dir As string, &sys_filename As string, &user_filename As string, &preserve_case As boolean, &html_header As string, &handle_errors As boolean) Returns number Local Field &field; Local string &key_string = ""; Local number &field_idx = 1; Local number &retcode; If &prefix_keys Then For &field_idx = 1 To &key_rec.FieldCount &field = &key_rec.GetField(&field_idx); If (&field.IsKey = True) Then If (All(&field.Value) And &field.IsRequired = True) Then If (&handle_errors) Then /* display a message if prefix_keys is true, but * user hasn't populated all the keys */ MessageBox(0, "", 137, 28, "Please attach the file after filling out " | "the required fields on this page."); End-If; REM ** return our own custom error code; Return - 1; Else &key_string = &key_string | &field.Value; End-If; End-If; End-For; REM ** remove spaces from keys; &key_string = Substitute(&key_string, " ", ""); /* PeopleBooks says the AddAttachment function URL must be 120 * characters or less and &user_filename must be less than 64 * characters. Furthermore, the ATTACHSYSFILENAME field is * defined as 128 characters. Therefore, we will limit the key * string to 64 characters: 64 for the keys and 64 for the name */ &sys_filename = Left(&key_string | &sys_filename, 64); End-If; REM ** prefix sys_filename with the ftp file path; &sys_filename = &sys_dir | &sys_filename; If (None(&html_header)) Then &html_header = GetHTMLText(HTML.APT_FILE_ATTACH_HEADER); End-If;
Chapter 2: The File Attachment API &retcode = AddAttachment(&url, &sys_filename, "", &user_filename, &max_filesize, &preserve_case, &html_header); /* AddAttachment returns the user_filename. sys_filename is a * combination of sys_filename and user_filename. */ &sys_filename = &sys_filename | &user_filename; If (&retcode %Attachment_Success) Then REM ** set filename vars to "" on failure; &user_filename = ""; &sys_filename = ""; End-If; /* PeopleTools delivers message catalog entries for the file * attachment API. It sure would be nice if the entry number * matched the return code */ If (&handle_errors) Then Evaluate &retcode When %Attachment_Cancelled MessageBox(0, "", 137, 3, "AddAttachment cancelled"); Break; When %Attachment_Failed MessageBox(0, "", 137, 2, "AddAttachment failed"); Break; When %Attachment_FileTransferFailed MessageBox(0, "", 137, 4, "AddAttachment failed: " | "File Transfer did not succeed"); Break; When %Attachment_NoDiskSpaceAppServ MessageBox(0, "", 137, 5, "AddAttachment failed: " | "No disk space on the app server"); Break; When %Attachment_NoDiskSpaceWebServ MessageBox(0, "", 137, 6, "AddAttachment failed: " | "No disk space on the web server"); Break; When %Attachment_FileExceedsMaxSize MessageBox(0, "", 137, 7, "AddAttachment failed: " | "File exceeds the max size"); Break; When %Attachment_DestSystNotFound MessageBox(0, "", 137, 8, "AddAttachment failed: " | "Cannot locate destination system for ftp"); Break; When %Attachment_DestSysFailedLogin MessageBox(0, "", 137, 9, "AddAttachment failed: " | "Unable to login into destination system for ftp"); Break;
71
72
PeopleSoft PeopleTools Tips & Techniques When %Attachment_FileNotFound MessageBox(0, "", 137, 29, "The file was not found so " | "the operation could not be completed."); Break; When %Attachment_NoFileName MessageBox(0, "", 137, 38, "AddAttachment failed: " | "No File Name Specified."); Break; End-Evaluate; End-If; Return &retcode; End-Function;
Note Please ignore the interesting string concatenation and line breaks in the long strings of the preceding code. Those “interesting” line breaks exist for formatting purposes only. The code will execute as shown. Nevertheless, you should avoid this practice when writing your own code. I placed several lines of documentation comments in the code for reference. Ignoring those for now, let’s walk through the executable code, line by line, starting with the call specification: Function APT_add_attachment(&url As string, &max_filesize As number, &prefix_keys As boolean, &key_rec As Record, &sys_dir As string, &sys_filename As string, &user_filename As string, &preserve_case As boolean, &html_header As string, &handle_errors As boolean) Returns number
The parameters to the APT_add_attachment function are similar to the original add_ attachment function. Let’s discuss the parameters that are different: ■■ &RECNAME We changed the &RECNAME parameter from a string to an actual record. As I previously mentioned, the &RECNAME parameter tells add_attachment where to find key values when creating a unique name in the chosen storage location. This change allows us to pass in a specific record from any level in the buffer without concern for context. In fact, if you prefer, you can even create a stand-alone record, populate the key values, and use that stand-alone record to provide key prefixes. If the key values plus the input value for &sys_filename are greater than 64 characters, this function will truncate &sys_filename at 64 characters prior to passing the value on to the native AddAttachment function. ■■ &sys_dir The parameter &sys_dir specifies a target directory on an FTP server. For example, if your FTP URL is defined as ftp://hrms.example.com/psfiles and the file needs to be stored in ftp://hrms.example.com/psfiles/invoices, then pass invoices/ as the value for this parameter. If you are not using FTP storage, then pass an empty string as the value for this parameter (““).
Chapter 2: The File Attachment API
73
■■ &sys_filename When calling APT_add_attachment, you may provide an additional prefix for &sys_filename by assigning a value to the &sys_filename parameter prior to calling the function. If you specify &prefix_keys and a value for &sys_filename, the final &sys_filename used to store the attachment will be a concatenation of the keys and the prefix specified in &sys_filename. ■■ &preserve_case The &preserve_case parameter tells the AddAttachment function to maintain the case of the original filename. This may be important when working with files on systems with case-sensitive filenames. ■■ &html_header The &html_header parameter allows us to add HTML to the File Attachment API upload page. This additional HTML may contain a basic header, some instructions, or even some JavaScript validation logic. ■■ &handle_error The &handle_error parameter replaces the &MESSAGE_LVL parameter from the original add_attachment function. A Boolean value to show or not show an error message is satisfactory. We’ll leave the “show message on success” case to the code that actually called APT_add_attachment. While it would certainly be possible to leave error handling entirely up to the caller, I find the centralized evaluate statement and corresponding message catalog references to be very helpful. ■■ &RETCODE Unlike the delivered version, this version returns the AddAttachment return value as the function’s return value. The delivered add_attachment function uses the input parameter &RETCODE to specify the AddAttachment return value. Let’s skip the variable declarations and move on to the first major block of code: If &prefix_keys Then For &field_idx = 1 To &key_rec.FieldCount &field = &key_rec.GetField(&field_idx); If (&field.IsKey = True) Then If (All(&field.Value) And &field.IsRequired = True) Then If (&handle_errors) Then /* display a message if prefix_keys is true, but * user hasn't populated all the keys */ MessageBox(0, "", 137, 28, "Please attach the file after filling out " | "the required fields on this page."); End-If; /* The user did not populate all required keys. * Return our own custom error code */ Return - 1; Else &key_string = &key_string | &field.Value; End-If; End-If; End-For; REM ** remove spaces from keys; &key_string = Substitute(&key_string, " ", ""); /* PeopleBooks says the AddAttachment function URL must be 120
74
PeopleSoft PeopleTools Tips & Techniques * characters or less and &user_filename must be less than 64 * characters. Furthermore, the ATTACHSYSFILENAME field is * defined as 128 characters. Therefore, we will limit the key * string to 64 characters: 64 for the keys and 64 for the name */ &sys_filename = Left(&key_string | &sys_filename, 64); End-If;
This block is responsible for copying the key values from &keys_rec into the target filename. It does so by looping through the fields in &keys_rec. The code checks for a value in each required key field. If a required key field does not have a value, then it displays a message and returns -1. The PeopleBooks documentation for the AddAttachment function says that the value passed into AddAttachment for the UserFile parameter cannot exceed 64 characters in length. It does not give guidance for the DirAndFileName parameter. However, looking at the FILE_ATTACH_SBR subrecord, we can see that the maximum value for ATTACHSYSFILENAME is 128 characters. Since the final value for ATTACHSYSFILENAME is a combination of the original ATTACHSYSFILENAME value and the ATTACHUSERFILE value, and if ATTACHUSERFILE can contain 64 characters, it stands to reason that any prefix given to ATTACHSYSFILENAME must be 64 characters or less. Therefore, the last executable line in this block truncates the &sys_filename value to a maximum of 64 characters. At this juncture, &sys_filename represents only the prefix we will pass into AddAttachment. AddAttachment will prefix the chosen filename with this value prior to sending it to the storage location (FTP or record). As you can see from this code, we’re prefixing &sys_filename with the keys, so, if you pass in a value for &sys_filename and specify &prefix_keys, then the key values will precede the prefix specified by &sys_filename. Of course, you could override this by either prefixing the keys yourself or changing the code in this function. &sys_filename = &sys_dir | &sys_filename;
This is the final step required to prepare the &sys_filename variable for the AddAttachment function. If you specify an FTP URL and need to place the file in a subfolder of the URL, then pass that folder name into APT_add_attachment as the value for the parameter &sys_dir. The value must end in / because the filename will be appended to this value to derive the target path and file. If (None(&html_header)) Then &html_header = GetHTMLText(HTML.APT_FILE_ATTACH_HEADER); End-If;
This section provides default HTML for the header. This HTML will display in the File Attachment API upload page prior to the input field and Browse button. The following is the HTML definition of APT_FILE_ATTACH_HEADER. Create this HTML definition so the attachment function will be ready for testing. Attach a file
Select a file using the <strong>Browse button and then click <strong>Upload
Chapter 2: The File Attachment API
75
HTML Definitions An HTML definition is a text definition maintained within App Designer. Using App Designer, you can create HTML definitions to store HTML, JavaScript, CSS, XML, or any other type of text. At runtime, you can extract that text using the GetHTMLText() PeopleCode function. HTML definitions can have bind variables and meta-HTML sequences. This makes HTML definitions an excellent choice for templates. Unfortunately, HTML definitions are available only to PeopleCode running online. I have often wished for the template capabilities of HTML definitions when writing web service handlers or App Engine programs. When writing HTML in HTML definitions, be sure to use the same style class attributes as PeopleSoft does for similar items. For example, when creating hyperlinks, use the style class PSHYPERLINK. Notice that in the APT_FILE_ATTACH_HEADER HTML definition, we use PAPAGETITLE and PSTEXT to provide styling for HTML. Following this guideline will ensure that your custom HTML blends in with the rest of the PeopleSoft-generated HTML. Furthermore, if you change the style definitions for your PeopleSoft application, the visual appearance of your custom HTML will change accordingly. We will make extensive use of HTML definitions in Part II of this book. &retcode = AddAttachment(&url, &sys_filename, "", &user_filename, &max_filesize, &preserve_case, &html_header); /* AddAttachment returns the user_filename. sys_filename is a * combination of sys_filename and user_filename. */ &sys_filename = &sys_filename | &user_filename;
This block calls AddAttachment and then sets the output parameter &sys_filename to the value that is used to identify the attachment within the chosen storage location (FTP or record). If (&retcode %Attachment_Success) Then REM ** set filename vars to "" on failure; &user_filename = ""; &sys_filename = ""; End-If;
If the attachment failed, then clear the output parameters &user_filename and &sys_ filename. /* PeopleTools delivers message catalog entries for the file * attachment API. It sure would be nice if the entry number * matched the return code */ If (&handle_errors) Then Evaluate &retcode When %Attachment_Cancelled
76
PeopleSoft PeopleTools Tips & Techniques MessageBox(0, "", 137, 3, "AddAttachment cancelled"); Break; ... End-Evaluate; End-If; Return &retcode; End-Function;
Finish the function by evaluating the return code for errors and display them as needed. Finally, return the AddAttachment result. Let’s update the ATTACHADD button PeopleCode to use our new function and then compare the difference in the File Attachment API upload page. Note The following code is a complete replacement for the ATTACHADD FieldChange PeopleCode we wrote earlier in this chapter. Declare Function APT_add_attachment PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Local number &retcode = APT_add_attachment( URL.APT_WEB_ASSET_ATTACHMENTS, 0, True, GetRecord(Record.APT_WEB_ASSETS), "", APT_WA_ATTACH.ATTACHSYSFILENAME, APT_WA_ATTACH.ATTACHUSERFILE, False, "", True); If (&retcode = %Attachment_Success) Then APT_set_buttons_state(APT_WA_ATTACH.ATTACHUSERFILE, GetRecord(Record.FILE_ATTACH_WRK)); End-If;
Figure 2-21 shows the File Attachment API upload page after creating the HTML definition and updating the ATTACHADD PeopleCode.
Chapter 2: The File Attachment API
77
FIGURE 2-21. File Attachment API upload page with custom HTML
Moving to Level 1
Let’s use the concepts learned thus far to add attachments to another component. Suppose that our human resources department requires photocopies of employees’ driver’s licenses. That department wants to attach scanned copies of driver’s licenses to the Driver’s License Data component in PeopleSoft. You can find the Driver’s License component in the PeopleSoft HRMS application, by navigating to Workforce Administration | Personal Information | Biographical | Driver’s License Data. This request will require us to modify the Driver’s License Data page, named DRIVERS_LIC_ GBL. Following best practices, we want to keep modifications to a minimum. If we design the modification properly, this page is the only piece of the component we will need to modify.
Modifying the Page Rather than adding the ATTACHSYSFILENAME and ATTACHUSERFILE fields to the DRIVERS_ LIC record, which is the primary record at level 1, we will create a record with the same keys plus the FILE_ATTACH_SBR subrecord. Name this new record APT_DL_ATTACH. We will also need a record to store attachments, so create the record APT_DL_ATTDET, as shown in Figure 2-22.
78
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-22. Driver’s License Data attachment records Next, we will add the attachment fields to the page DRIVERS_LIC_GBL. Our first step is to make some room on the Driver’s License Data page by expanding the Driver’s License Information scroll area and moving the License Type scroll area farther down the page. Next, we add the ATTACHUSERFILE field to the page, and then set the label, size, and display-only attributes. When you save the page, App Designer will respond by presenting you with the error, “More than one data record in scroll—make fields from non-primary record related display,” as shown in Figure 2-23. It turns out that adding APT_DL_ATTACH.ATTACHUSERFILE would place two data records inside the same scroll area. This is not allowed by App Designer. The approach we took of adding a second data record to the buffer is similar to the approach we used with the Web Assets page example. What makes this different from the earlier example? In this example, we are trying to add another data record to a scroll area. The Web Assets page example added a second data record to level 0. We can still add attachments to this component, but we need to modify our approach. Since we can have only one data record per scroll area, one option is to modify the existing data record by adding the FILE_ATTACH_SBR subrecord to that data record. In this case, that would require us to modify the DRIVERS_LIC record. Modifying delivered record definitions, however, is extremely risky. If you add or remove fields from a delivered record, then you may need to
Chapter 2: The File Attachment API
79
FIGURE 2-23. Multiple data records in scroll error modify every view and SQL statement that uses that record. For example, if we add these two fields to DRIVERS_LIC and there is an App Engine program that inserts data into DRIVERS_LIC, then that App Engine will fail unless we add those two fields to the App Engine’s SQL INSERT statements. As an alternative, we can add the ATTACHUSERFILE and ATTACHSYSFILENAME fields to a derived/work record and display those derived/work fields on the page. After we put those fields on a page, we will need to figure out how to get those values into the database. Let’s choose this alternative since it requires fewer modifications to delivered objects. First, create a new derived/work record and add the fields ATTACHSYSFILENAME and ATTACHUSERFILE. Save the new record as APT_DL_ATT_WRK. Update the attachment field on the page DRIVERS_LIC_GBL to use this new derived/work record and save the page. (Note that changing a field’s source resets its field label.) This resolves the “Multiple data records” error, because derived/work records are not data records. This derived/work record approach allows us to display data on a page, but we will need to use PeopleCode to save and load the derived/work record’s data.
80
PeopleSoft PeopleTools Tips & Techniques Note We could have added FILE_ATTACH_SBR to APT_DL_ ATT_WRK. However, we will later add PeopleCode to the ATTACHSYSFILENAME RowInit event. Using FILE_ATTACH_SBR would require us to modify the PeopleCode for that delivered record. We also need to consider the Add, View, and Delete buttons. In the Web Assets page example, we added PeopleCode to the Web Assets component to provide attachment functionality. We could make the same change to the Driver’s License Data component, but that would add one more item to the list of modified definitions. Fewer modified definitions simplifies patches and upgrades. As an alternative to modifying the component, we could add FieldChange PeopleCode to the buttons’ derived work record. In the Web Assets page example, the buttons’ derived/work record was a delivered record. Adding code to those FieldChange events would add another definition to the list of modified definitions. To avoid modifying the FILE_ATTACH_WRK record, we can add the ATTACHADD, ATTACHDELETE, and ATTACHVIEW fields to APT_DL_ATT_WRK. Figure 2-24 shows the APT_DL_ATT_WRK record with the attachment data and button fields.
FIGURE 2-24. The APT_DL_ATT_WRK derived/work record
Chapter 2: The File Attachment API
81
FIGURE 2-25. Modified DRIVERS_LIC_GBL page in App Designer Add APT_DL_ATT_WRK.ATTACHSYSFILENAME to the page and make it invisible. Add three buttons and associate them with the ATTACHADD, ATTACHVIEW, and ATTACHDELETE fields of the APT_DL_ATT_WRK derived/work record. Figure 2-25 shows the lower portion of the modified DRIVERS_LIC_GBL page. Test the page to make sure everything appears as expected. It should look like Figure 2-26. We have now completed the user interface modifications and can implement the three attachment buttons’ FieldChange PeopleCode. The PeopleCode for these buttons will require a URL definition for the attachment record. Just as you did for the Web Assets example, navigate to PeopleTools | Utilities | Administration | URLs. Add the URL definition APT_DL_ATTACHMENTS and give it the value record://APT_ DL_ATTDET.
Adding the PeopleCode It is time to add PeopleCode to this component. As I previously mentioned, we will break from the best practice of adding component-specific PeopleCode to component definitions for the purpose of avoiding another modification. For the Add button, we can start with the final code for the Web Assets page ATTACHADD button and modify a copy of that code as necessary. The difference between this example and the Web
82
PeopleSoft PeopleTools Tips & Techniques
FIGURE 2-26. Online view of the modified DRIVERS_LIC_GBL component Assets example is that we will need to update a derived/work record and a database table. In the Web Assets example, we were able to include the database table directly in the component buffer. To implement the Add button, open the PeopleCode editor for the APT_DL_ATT_WRK. ATTACHADD field and switch to the FieldChange event. Add the following PeopleCode. Deviations from the Web Assets example are shown in bold. Declare Function APT_add_attachment PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Local Record &keys_rec = CreateRecord(Record.APT_DL_ATTACH); &keys_rec.EMPLID.Value = DRIVERS_LIC.EMPLID; &keys_rec.DRIVERS_LIC_NBR.Value = DRIVERS_LIC.DRIVERS_LIC_NBR; Local number &retcode = APT_add_attachment(URL.APT_DL_ATTACHMENTS, 0, True, &keys_rec, "", APT_DL_ATT_WRK.ATTACHSYSFILENAME, APT_DL_ATT_WRK.ATTACHUSERFILE, False, "", True);
Chapter 2: The File Attachment API
83
If (&retcode = %Attachment_Success) Then REM ** Populate original values. See Record.Save in PeopleBooks; &keys_rec.SelectByKey( False); &keys_rec.ATTACHSYSFILENAME.Value = APT_DL_ATT_WRK.ATTACHSYSFILENAME; &keys_rec.ATTACHUSERFILE.Value = APT_DL_ATT_WRK.ATTACHUSERFILE; If (&keys_rec.Save()) Then APT_set_buttons_state(APT_DL_ATT_WRK.ATTACHUSERFILE, GetRecord(Record.APT_DL_ATT_WRK)); Else MessageBox(0, "", 0, 0, "Failed to save the attachment to the database"); REM ** remove orphaned attachment from the database; &retcode = DeleteAttachment(URL.APT_DL_ATTACHMENTS, APT_DL_ATT_WRK.ATTACHSYSFILENAME); End-If; End-If;
Before we go any further with this example, we should implement RowInit PeopleCode to enable and disable the attachment buttons. As part of this PeopleCode, we will load the ATTACHSYSFILENAME and ATTACHUSERFILE derived/work fields with data from the off-screen attachment table (APT_DL_ATTACH, the table we initially added to level 1, which resulted in a second record at level 1). As with the previous code listing, deviations from the Web Assets component are shown in bold. Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Local Record &keys_rec = CreateRecord(Record.APT_DL_ATTACH); &keys_rec.EMPLID.Value = DRIVERS_LIC.EMPLID; &keys_rec.DRIVERS_LIC_NBR.Value = DRIVERS_LIC.DRIVERS_LIC_NBR; &keys_rec.SelectByKey(); APT_DL_ATT_WRK.ATTACHSYSFILENAME = &keys_rec.ATTACHSYSFILENAME.Value; APT_DL_ATT_WRK.ATTACHUSERFILE = &keys_rec.ATTACHUSERFILE.Value; APT_set_buttons_state(APT_DL_ATT_WRK.ATTACHUSERFILE, GetRecord(Record.APT_DL_ATT_WRK));
Note The RowInit attachment button PeopleCode will have no effect until you add data to the APT_DL_ATTACH record through the component’s new file attachment buttons.
84
PeopleSoft PeopleTools Tips & Techniques Next, let’s implement the View button. Copy the following code into the FieldChange event of record field APT_DL_ATT_WRK ATTACHVIEW. The only difference between this code and the Web Assets page View button attachment code is the name of the records. Declare Function view_attachment PeopleCode FILE_ATTACH_WRK.ATTACHVIEW FieldChange; Local number &retcode; view_attachment(URL.APT_DL_ATTACHMENTS, APT_DL_ATT_WRK.ATTACHSYSFILENAME, APT_DL_ATT_WRK.ATTACHUSERFILE, 2, &retcode);
Note Both examples in this chapter use view_attachment, a PeopleCode wrapper for the native ViewAttachment function. In Chapter 5 you will learn an alternative download method. The method described in Chapter 5 gives you complete control over the way PeopleSoft presents an attachment to the user. We have one button left to implement: the Delete button. The code for the Delete button follows the same pattern as the Add button. We start with the code for the Web Assets page Delete button and then add code for the APT_DL_ATTACH record, since it is not in the buffer. Add the following code to the FieldChange event of the ATTACHDELETE field in the APT_DL_ ATT_WRK record. The new code (shown in bold) creates a record object based on APT_DL_ ATTACH, so we can delete data that does not exist in the component buffer. Declare Function delete_attachment PeopleCode FILE_ATTACH_WRK.ATTACHDELETE FieldChange; Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Local number &retcode; Local Record &keys_rec; delete_attachment(URL.APT_DL_ATTACHMENTS, APT_DL_ATT_WRK.ATTACHSYSFILENAME, APT_DL_ATT_WRK.ATTACHUSERFILE, 2, &retcode); If (&retcode = %Attachment_Success) Then &keys_rec = CreateRecord(Record.APT_DL_ATTACH); &keys_rec.EMPLID.Value = DRIVERS_LIC.EMPLID; &keys_rec.DRIVERS_LIC_NBR.Value = DRIVERS_LIC.DRIVERS_LIC_NBR; &keys_rec.Delete(); APT_set_buttons_state(APT_DL_ATT_WRK.ATTACHUSERFILE, GetRecord(Record.APT_DL_ATT_WRK)); End-If;
Chapter 2: The File Attachment API
85
Attachments with Document Management Systems The best place to store attachments is in a document management system like Oracle’s Universal Content Management System (UCM). Document management systems support retention policies, indexing, searching, versioning, and many other valuable features. Document management systems support a variety of integration protocols including WebDAV and web services. Integrating PeopleSoft with UCM, for example, can be as simple as generating the correct HTML links. Adding attachments to UCM can be facilitated by adding a link to the appropriate UCM folder and document upload page. You can display UCM attachments in PeopleSoft by consuming a UCM search web service, providing a transaction’s keys as parameters. Opening attachments is just as trivial. The UCM search web service returns a document ID, and all you need to do is provide users with a link to that UCM document. Another way to integrate with a document management system is to modify the File Attachment API. The File Attachment API offers only record and FTP repositories. As you have seen, however, we rarely call the native PeopleCode attachment functions directly. Rather, we wrap the native functions in custom functions that enhance the functionality of the native functions. This abstraction gives us a hook to create additional URL protocol handlers. For example, you could customize the APT_add_attachment function to handle URLs prefixed with webdav:// or ucm://.
You should now be able to add, view, and delete attachments from the Driver’s License Data component.
Adding Multiple Attachments per Transaction
You can add multiple attachments to a transaction by deviating slightly from the Web Assets example. In the Web Assets example, we added the attachment fields and buttons to level 0. If you want to add multiple attachments to a transaction, create a new grid or scroll area within the level that will hold the attachments and add the attachment fields and buttons to that new grid or scroll area. When you create the record containing the transaction’s keys, make sure you have all the keys of the higher-level scroll area and don’t add FILE_ATTACH_SBR. Instead, add ATTACHSYSFILENAME and ATTACHUSERFILE directly. By making ATTACHSYSFILENAME a key field, you will effectively make your attachment record a child record of the higher-level scroll area. Remember the rule for parent/child row sets: A child row set contains all of the key fields of the parent row set plus at least one additional field.
Processing Attachments
The ability to store records with transactions is of great value and can certainly expedite manual transaction processing when all supporting documentation is attached to the transaction. But there are other uses for attachments. For example, rather than requiring users to save imported files into specific directories or save them in specific formats, you can provide a friendlier user experience by offering users the ability to upload imported files to run control records.
86
PeopleSoft PeopleTools Tips & Techniques
Accessing Attachments After a user uploads a file, we need a way to access that file. PeopleSoft provides the GetAttachment function for this specific purpose. The following listing contains the GetAttachment function signature: GetAttachment(URL.URLID, &AttachSysFileName, &LocalFileName [, &LocalDirEnvVar[, &PreserveCase]])
Like the attachment functions we used earlier in this chapter, the GetAttachment function requires a URL, which identifies the attachment storage location. The function also requires a pointer to the attachment within that storage location (AttachSysFileName). The purpose of this function is to copy a file from the attachment repository into the local file system. The local file system is the app server, process scheduler server, or even your workstation (if you are running an App Engine locally). In order to copy the file to the local system, the GetAttachment function needs a local filename. Use the &LocalFileName parameter to specify the target filename on the local file system. Rather than hard-code the local file path, the GetAttachment function allows you to specify an environment variable using the &LocalDirEnvVar parameter. For example, you could specify TEMP as the environment variable to have GetAttachment write files into your local temp directory.
Working Directories Some file systems use / as the path delimiter, and some use \. Some file systems, like Windows, use drive letters; other file systems do not. Considering that a process may run on a Solaris app server, a Linux process scheduler server, or a Windows process scheduler server, how should you code the file path? The process scheduler actually maintains a table with file path information. For example, if you want GetAttachment to place files in a process’s designated file location, then execute the following SQL: SELECT PRCSOUTPUTDIR FROM PSPRCSPARMS WHERE PRCSINSTANCE = %ProcessInstance
Files placed in this directory by an App Engine will be available to the user through the View Log/Trace link in the Process Monitor. Likewise, the application server has a couple of standard environment variables you can use to create files in specific locations. For example, PS_FILEDIR and PS_SERVDIR point to file storage locations on the application server. Your server administrator can create additional environment variables as needed. Another method for creating parameterized file paths is to use a database table. The benefit of the environment variable approach over the table-driven approach is that multiple servers on various operating systems will share the same database. Some file functions, such as GetFile, do not use environment variables. When using these functions, you can still access the value of environment variables by using the PeopleCode functions GetEnv and ExpandEnvVar.
Chapter 2: The File Attachment API
87
Storing Attachments PutAttachment is the non-user interface complement to AddAttachment. It allows you to add files to an attachment storage location in batch processes or even integrations. If you add attachments to transactions using PutAttachment and want users to be able to view those attachments online, be sure to update the transaction’s ATTACHSYSFILENAME and ATTACHUSERFILE fields. The PutAttachment function has syntax similar to the GetAttachment function: PutAttachment(URL.URLID, &AttachSysFileName, &LocalFileName [, &LocalDirEnvVar[, &PreserveCase]])
PutAttachment is best used with integrations, implementations, and migrations.
Implementing File Attachment Validation
With the exception of file size constraints, the delivered File Attachment API offers little in the form of file attachment file type and content validation. Nevertheless, you can write your own file attachment validation routines.
Filename Validation The attachment functionality we added to the Driver’s License Data component expects image files. Therefore, it is appropriate to verify the uploaded file’s type. Following a common filevalidation technique, we can compare the attachment file’s extension against a list of valid image file extensions. The following code listing is a modified version of the Driver’s License Data ATTACHADD FieldChange PeopleCode stored in the APT_DL_ATT_WRK record. Modifications are displayed in bold type. This listing verifies that the uploaded file’s extension is within a list of acceptable image file extensions. Declare Function APT_add_attachment PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Declare Function APT_set_buttons_state PeopleCode APT_ATTACH_FUNC.ATTACHADD FieldFormula; Local Local Local Local Local Local
Record &keys_rec = CreateRecord(Record.APT_DL_ATTACH); array of string &extensions; string &ext; string &file_name; number &ext_idx = 0; boolean &is_image_file = False;
&keys_rec.EMPLID.Value = DRIVERS_LIC.EMPLID; &keys_rec.DRIVERS_LIC_NBR.Value = DRIVERS_LIC.DRIVERS_LIC_NBR; Local number &retcode = APT_add_attachment(URL.APT_DL_ATTACHMENTS, 0, True, &keys_rec, "", APT_DL_ATT_WRK.ATTACHSYSFILENAME, APT_DL_ATT_WRK.ATTACHUSERFILE, False, "", True);
88
PeopleSoft PeopleTools Tips & Techniques If (&retcode = %Attachment_Success) Then REM ** Validate file name; &file_name = APT_DL_ATT_WRK.ATTACHUSERFILE; &extensions = Split("bmp gif jpeg jpg png tif tiff"); While &extensions.Next(&ext_idx) &ext = &extensions [&ext_idx]; If (&ext = Lower(Right(&file_name, Len(&ext)))) Then &is_image_file = True; Break; End-If; End-While; If (&is_image_file) Then REM ** Populate original values. See Record.Save in PeopleBooks; If ( Not &keys_rec.SelectByKey( False)) Then REM ** if row not found, SelectByKey will reset key values; &keys_rec.EMPLID.Value = DRIVERS_LIC.EMPLID; &keys_rec.DRIVERS_LIC_NBR.Value = DRIVERS_LIC.DRIVERS_LIC_NBR; End-If; &keys_rec.ATTACHSYSFILENAME.Value = APT_DL_ATT_WRK.ATTACHSYSFILENAME; &keys_rec.ATTACHUSERFILE.Value = APT_DL_ATT_WRK.ATTACHUSERFILE; If (&keys_rec.Save()) Then APT_set_buttons_state(APT_DL_ATT_WRK.ATTACHUSERFILE, GetRecord(Record.APT_DL_ATT_WRK)); Else MessageBox(0, "", 0, 0, "Failed to save the attachment to the database"); REM ** remove orphaned attachment from the database; &retcode = DeleteAttachment(URL.APT_DL_ATTACHMENTS, APT_DL_ATT_WRK.ATTACHSYSFILENAME); End-If; Else MessageBox(0, "", 0, 0, "This component only accepts image files. " | "Please attach an image file."); REM ** remove orphaned attachment from the database; &retcode = DeleteAttachment(URL.APT_DL_ATTACHMENTS, APT_DL_ATT_WRK.ATTACHSYSFILENAME); REM ** reset the file attachment work fields; APT_DL_ATT_WRK.ATTACHSYSFILENAME.SetDefault(); APT_DL_ATT_WRK.ATTACHUSERFILE.SetDefault(); End-If; End-If;
Chapter 2: The File Attachment API
89
This code provides minimal assurance as to the type of file uploaded. A full validation would involve reading and validating the contents of the attached image file. Just like the file size constraint provided by the File Attachment API, the preceding filename validation code does not execute until after the image is transferred to the server. Depending on the file’s size and network conditions, this could have a significant impact on the user’s experience.
File Contents Validation If the uploaded file is a text file, we can validate the contents of that file by copying the file to a temporary file and investigating its contents. For example, if you expect an XML file that conforms to a specific Document Type Definition (DTD), you can use the GetAttachment PeopleCode function to copy the attachment to a temporary file and then use the PeopleCode XmlDoc object to validate that XML. Unfortunately, PeopleCode does not offer methods for reading binary files. The Java language, however, provides full support for binary files, as well as support for specific binary file formats. You will learn how to integrate Java with PeopleCode in Chapter 9. Many database platforms also contain functions for reading binary data from database tables. If your storage location is a database record definition, you may be able to use your database’s procedural language to validate the contents of attachments.
Conclusion
This chapter walked through two File Attachment API examples and provided some file attachment and PeopleCode tips. Armed with this new understanding of the File Attachment API, you will be able to add attachments to any PeopleSoft component. You will see references to this chapter in several chapters that follow. In Chapters 5, 6, and 7, we will add new web assets to the Web Assets component.
This page intentionally left blank
Chapter
3
Approval Workflow Engine
92
PeopleSoft PeopleTools Tips & Techniques
P
urchase requisitions, training requests, and employee transfers are some of the many enterprise transactions that require approval. Some transactions, such as training requests, may require a single-level manager or supervisor approval; other transactions may use complex routing rules to determine the approval path. For example, a purchase requisition for an item costing $10,000 or less may require no more than a manager’s signature, whereas a purchase requisition for a single item costing $100,000 or more may require a senior vice president’s approval. Prior to electronic workflow systems, the approval process consisted of signed forms routed from one approver to the next. Prior to PeopleTools 8.48 and PeopleSoft 9.0, PeopleSoft applications used a PeopleTools approval framework called Virtual Approver. In PeopleTools 8.48, PeopleSoft introduced the Approval Workflow Engine (AWE). While some Financials and Supply Chain modules adopted AWE as early as version 8.8, the 9.0 version was the first to extensively use AWE. AWE began as a custom workflow engine for PeopleSoft’s Supply Chain Management module in version 8.8. The Supply Chain team continued to improve the workflow engine through release 8.9. In 9.0, many application development teams switched from Virtual Approver and legacy workflow to the new AWE. Since AWE is now a core component of PeopleTools 8.48, you can use it with any PeopleSoft application version. At the time of this writing, the best available documentation for AWE is the “Approval Workflow Engine (AWE) for HCM 9.0” and the “Delegation Framework for HCM v. 9.0” red papers available from Oracle’s support site, http://support.oracle.com. These red papers complement this chapter. In this chapter, you will learn how to workflow-enable transactions, reduce your modification footprint, and use AWE from web services and batch processes. The screenshots and navigation presented in this chapter were taken from a PeopleSoft HRMS 9.0 application. Even though AWE is a PeopleTools component that exists in every PeopleTools 8.48 and higher PeopleSoft application, the menu navigation for AWE metadata is application-specific. In HCM, the menu navigation for AWE is under Set Up HRMS | Common Definitions | Approvals. In Financials, it is under Set Up Financials | Supply Chain | Common Definitions | Approvals.
Workflow-Enabling Transactions
Workflow-enabling a transaction is a joint effort between developers and functional experts. Once a developer creates some supporting definitions, a functional expert can configure notifications and complex approval rules. This contrasts with the prior PeopleTools workflow engine, which required App Designer access to design and implement workflow activities, steps, rules, and routings. Workflow-enabling a transaction consists of the following tasks: ■■ Create supporting definitions: record, SQL, and application class definitions. ■■ Configure metadata. ■■ Modify the source transaction. In Chapter 2, we created a Web Asset component. Continuing that example, we want to add the requirement that prior to using a web asset, a member of the Portal Administrator group must approve the content and the web asset use case. In this chapter, we will use AWE to enhance the Web Assets page to ensure a web asset is approved before making it available for use.
Chapter 3: Approval Workflow Engine
93
Creating Supporting Definitions AWE uses queries, SQL definitions, and application classes to provide data and business logic for routing and notifications. It also uses custom record definitions to store information about the state of an approval.
Cross-Reference Record Transactions are composed of header and detail values. A transaction encompasses all of the data that represents a single unit. We identify a transaction through its header values. The level 0 scroll area, for example, is a transaction’s header record. Level 1 and all the levels below it make up the transaction’s detail records. Approving a transaction, therefore, requires approving the transaction header. AWE maintains approval information in its own transaction tables. The link between AWE’s transaction tables and the main transaction table is called the cross-reference record and is defined as containing the subrecord PTAFAW_XREF_SBR plus the transaction’s keys. The cross-reference record is similar to the attachment repository record we used in the previous chapter, in that we create the record and point PeopleTools to the record, but we never actually modify data within the record. Figure 3-1 shows our new web assets cross-reference record, named APT_WA_AWE_XREF. This will serve as the cross-reference record between web asset transactions and the corresponding AWE transaction. Save and build the APT_WA_AWE_XREF record.
FIGURE 3-1. Web assets cross-reference record
94
PeopleSoft PeopleTools Tips & Techniques
Transaction Approval Thread ID Each process has its own set of thread IDs. The AWE framework stores these thread IDs in a record named PTAFAW_IDS. Since we created a new cross-reference record, we will need to add a new row to the thread ID table, PTAFAW_IDS. AWE uses the thread ID as part of the crossreference table’s primary key. Since PeopleTools does not provide a user interface for maintaining thread IDs, we will need to add a new thread ID value using SQL. The following SQL inserts a new counter into the PTAFAW_IDS record: INSERT INTO PS_PTAFAW_IDS(PTAFCOUNTERNAME, PTAFAWCOUNTER) VALUES ('APT_WA_AWE_XREF', 1); / COMMIT /
The value you provide for PTAFAWCOUNTER is the initial value to be used for thread IDs. Consider it similar to the first check number when opening a new checking account. Once you set the value, you can never go back and change it. For a new checking account, you might choose to start your checks with a number like 1000, so recipients won’t question the integrity of your checking account. With thread IDs, the starting number doesn’t matter as much. Therefore, it seems reasonable to start thread IDs at 1. After we create our first web asset approval transaction, the AWE framework will begin incrementing this value.
Approval Event Handlers AWE triggers events throughout the approval cycle. For example, when a user submits a workflow transaction, AWE triggers the OnProcessLaunch event. We can handle these events by creating an event handler application class and registering the event handler with the approval process definition. The event handler pattern used by AWE is common to OOP. Java, for example, uses registered event handler callbacks to notify listeners as events arise. The difference between the AWE implementation and a Java implementation is that a Java object that triggers events may have multiple registered listeners, whereas AWE allows only one. At the end of this chapter, I will show you how to add this functionality with a custom event handler. Event handlers are subclasses of PTAF_CORE:ApprovalEventHandler. The AWE framework will call the appropriate event handler method as the event occurs during the course of a transaction’s life cycle. Since the default handler methods do not contain any business logic, you need to implement only the events required for your business process. The rest of the events will be handled by the base class methods, allowing you to ignore events that are irrelevant to your business process. Note The HRMS-specific documentation recommends subclassing the HMAF_AWE:Wrappers:ApprovalEventHandler class instead. Other modules and PeopleSoft applications may have similar recommendations. Consult your application-specific documentation for additional information. Since this example is meant to apply to any application that uses PeopleTools, it will use the base PTAF_CORE application classes.
Chapter 3: Approval Workflow Engine
95
The following code listing contains declarations for the common event handler methods. For a complete list, use App Designer to open the PTAF_CORE:ApprovalEventHandler application class’s PeopleCode editor. method method method method method method method
OnProcessLaunch(&appInst As PTAF_CORE:ENGINE:AppInst); OnStepComplete(&stepinst As PTAF_CORE:ENGINE:StepInst); OnStepPushback(&userinst As PTAF_CORE:ENGINE:UserStepInst); OnStepReactivate(&stepinst As PTAF_CORE:ENGINE:StepInst); OnFinalHeaderDeny(&appinst As PTAF_CORE:ENGINE:AppInst); OnHeaderDeny(&userinst As PTAF_CORE:ENGINE:UserStepInst); OnHeaderApprove(&appinst As PTAF_CORE:ENGINE:AppInst);
Notice that each of these event handlers has some type of instance parameter. The type of instance depends on the type of event. An approval event, for example, will receive an approval instance. A step event, which occurs when an approval moves from one step to the next in the approval chain, will receive an approval step instance. The instance parameter contains properties and methods allowing you access to relevant AWE and transaction information. For example, each of the instance classes has a thread property. The thread property corresponds to the cross-reference record we created earlier. Since the cross-reference record contains transaction header key values, event handlers have full access to the underlying transaction. In our example, we will implement approve and deny handlers. These handlers will update the status of a web asset. A web asset should not be available for use unless it is in approved status. Since AWE maintains approval history, it isn’t necessary to maintain a transaction-specific approved or denied flag. Nevertheless, I recommend maintaining an approval flag for convenience. Whether you are writing reports, processes, or integration points, you may not want to join your transactions to the AWE tables just to determine the approval state of a transaction. The only accurate way to maintain an approval flag is through an approval event handler. In this chapter, we will use the OnHeaderApprove and OnHeaderDeny event handler methods to maintain the transaction record’s approval flag. This event-driven design contrasts with the legacy workflow strategy, which involved updating the approval flag within an approval component. A key difference between AWE and legacy workflow is that AWE does not require a component, whereas the legacy workflow engine required a component to update workflow transactions. Let’s create our web asset event handler. We’ll need a new application package, as discussed in Chapter 1. Select File | New from the App Designer menu bar. When prompted for a definition type, select Application Package. Save this new application package with the name APT_WA_AWE. This new package will contain event handlers, user lists, criteria definitions, and many other AWE-related application classes. After saving the new application package, add a new class named WebAssetAppr_EventHandler. Add the following PeopleCode to this new class: import PTAF_CORE:ApprovalEventHandler; import PTAF_CORE:ENGINE:AppInst; import PTAF_CORE:ENGINE:UserStepInst; import PTAF_CORE:ENGINE:Thread; class WebAssetAppr_EventHandler extends PTAF_CORE:ApprovalEventHandler method OnHeaderApprove(&appinst As PTAF_CORE:ENGINE:AppInst); method OnHeaderDeny(&userinst As PTAF_CORE:ENGINE:UserStepInst); private
96
PeopleSoft PeopleTools Tips & Techniques method UpdateStatus(&thread As PTAF_CORE:ENGINE:Thread, &status As string); end-class; method OnHeaderApprove /+ &appinst as PTAF_CORE:ENGINE:AppInst +/ /+ Extends/implements PTAF_CORE:ApprovalEventHandler.OnHeaderApprove +/ %This.UpdateStatus(&appinst.thread, "A"); end-method; method OnHeaderDeny /+ &userinst as PTAF_CORE:ENGINE:UserStepInst +/ /+ Extends/implements PTAF_CORE:ApprovalEventHandler.OnHeaderDeny +/ %This.UpdateStatus(&userinst.thread, "D"); end-method; method UpdateStatus /+ &thread as PTAF_CORE:ENGINE:Thread, +/ /+ &status as String +/ /* &thread.recname contains the header record name, but we are * using a sibling record so we have to hard code the record name */ Local Record &asset_rec = CreateRecord(Record.APT_WA_APPR); /* &thread.rec contains the cross reference record which has * header record keys */ &thread.rec.CopyFieldsTo(&asset_rec); /* If we were updating the header record, then we could use the * following convenience method. */ REM &thread.SetAppKeys(&asset_rec); &asset_rec.SelectByKey(); &asset_rec.GetField(Field.APPR_STATUS).Value = &status; &asset_rec.Update(); end-method;
Let’s walk through this code segment by segment, starting with the imports. We must import the base class, PTAF_CORE:ApprovalEventHandler. The instance imports, PTAF_CORE:ENGINE:AppInst and PTAF_CORE:ENGINE:UserStepInst, represent various parameters to the event handler’s methods. The PTAF_CORE:ENGINE:Thread application class provides us with access to the transaction header. The next segment represents our event handler’s application class definition. This looks similar to the class definitions we created in Chapter 1. Of all the methods defined in
Chapter 3: Approval Workflow Engine
97
the superclass, PTAF_CORE:ApprovalEventHandler, we declare only the methods (events) we want to handle. Our approve and deny event handlers use the same PeopleCode and SQL to update the web asset header record. Rather than duplicate this code in each event, we centralize the code in a private method named UpdateStatus.
E-Mail Templates When a developer adds a new value to the Web Assets component, we want the workflow engine to send an e-mail message to members of the Portal Administrator group. The message should look like this: Subject: Web Asset Approval I created a new web asset named EMPLOYEE_SHELF and would appreciate prompt approval at your earliest convenience. The web asset’s details: Content Type: application/x-shockwave-flash Filename: empl-shelf.swf Description: The employee shelf is a Flex component based on the DisplayShelf Flex component. This component displays photos of a manager’s direct reports in a fashion similar to iTunes cover art. You may approve this transaction at http://hrms.example.com/psp/hrms/EMPLOYEE/HRMS/c/ APT_CUSTOM.APT_WEB_ASSETS.GBL?Page=APT_WEB_ASSETS&Action=U&ASSET_ ID=EMPLOYEE_SHELF This e-mail message contains several pieces of transaction-specific data. AWE allows us to use template bind variables to insert transaction data at runtime. AWE’s templating system looks very similar to message catalog bind variables. The following text describes the same e-mail template as the preceding message, except that the transaction information is replaced with bind variables. We will add this template to the AWE metadata soon. Subject: Web Asset Approval I created a new web asset named %2 and would appreciate prompt approval at your earliest convenience. The web asset’s details: Content Type: %3 Filename: %4 Description: %5 You may approve this transaction at %1 E-mail template bind variables correspond to selected fields from a SQL definition, with this difference: The %1 bind variable is reserved for the approval page URL. Therefore, the SQL statement’s column 1 equates to bind variable %2. Note Some workflow e-mail messages exist for notification purposes only. If you create a template that should not include a URL, you can leave the %1 bind variable out of your template.
98
PeopleSoft PeopleTools Tips & Techniques
SQL Joins PeopleSoft databases offer the following SQL join options: ■■ WHERE clause criteria joins ■■ ANSI join syntax Which is better? It is really a matter of style and personal (or organizational) preference. I find that ANSI join syntax better clarifies the intent of a join by providing a separation between joins and actual criteria. It is easier for me to miss a join field when mixing join statements with actual filter criteria. PeopleSoft’s documentation and source code are full of WHERE clause join examples. Rarely do we see an ANSI join example. In this book, where appropriate, I have used ANSI joins instead of WHERE clause criteria joins to provide you with examples of this alternative join syntax.
The e-mail template identifies transactional data elements. AWE uses SQL definitions to provide data for each template variable except %1. An AWE e-mail template SQL definition must return a column for each bind variable except %1, and must contain SQL bind parameters for each transaction header key field. The following SQL selects the transaction values required by this template. Create this SQL definition in App Designer and name it APT_WA_EMAIL_BIND. SELECT , , , FROM INNER ON WHERE
WA.ASSET_ID WA.MIMETYPE ATT.ATTACHUSERFILE WA.DESCR PS_APT_WEB_ASSETS WA JOIN PS_APT_WA_ATTACH ATT WA.ASSET_ID = ATT.ASSET_ID WA.ASSET_ID = :1
To create an e-mail template, open your browser and log in to your PeopleSoft application. Navigate to PeopleTools | Workflow | Notifications | Generic Templates and add the new value APT_WebAsset_AwaitingAppr. Use Figure 3-2 as a guide for creating this template. After adding text to this template, fill in the Template Variables grid with a description of each of the bind variables. This will serve as documentation for you and others who may need to modify this template at a later time. While we are here, let’s create Approve and Deny notification templates. For the approved request template, add the value APT_WebAsset_Approved and this message: Sender: Subject: Web Asset Approved I approved the new web asset named %2. You can view this transaction at %1.
Chapter 3: Approval Workflow Engine
99
FIGURE 3-2. Web asset approval request e-mail template
For the denied request template, add the value APT_WebAsset_Denied and this message: Sender: Subject: Web Asset Denied I denied your request to approve the new web asset named %2. Please feel free to contact me if you have questions. You can view this transaction at %1. Since both of these templates contain the same bind variables, they can share the same SQL definition. Create a SQL definition in App Designer named APT_WA_EMAIL_NOTIFY_BIND that has the following SQL: SELECT :1 FROM PS_INSTALLATION
Unlike the approval request template, our notification templates require only transaction keys. Rather than waste CPU cycles searching for values in transaction tables, we can move the transaction
100
PeopleSoft PeopleTools Tips & Techniques
key bind values into the SELECT clause, and then specify PeopleSoft’s common one-row table, PS_INSTALLATION, in the FROM clause.
User Lists When a transaction is submitted to the AWE, it needs to know the intended recipients. We identify the intended recipient through a definition called a user list. User lists define a collection of operator IDs. Each application delivers a set of predefined user lists based on transaction data within that application. For example, the HRMS application delivers user lists generated from the organization’s direct reports hierarchy. We can create user lists from roles, SQL definitions, queries, or application classes. Roles provide the simplest configuration but the least flexibility (unless you are using dynamic roles). Queries and SQL definitions provide more flexibility because they allow you to use SQL logic to select a list of operator IDs. When using queries as user lists, be sure to save the query as a process query, rather than the standard user query. Of the user list types, application classes offer the most flexibility. Through SQL and PeopleCode objects, application classes can combine and/or evaluate the results of the other three types of user lists. Application class user lists offer one more significant feature: a hook into the approval process. By using an application class, you can update other transactions, log information about the approval process, or even execute a web service. By convention, however, it is best to keep user lists as user lists, and use event handlers to perform other operations. For demonstration purposes, we will use an application class user list. Application class user lists must conform to the following contract: ■■ They must extend the base class PTAF_CORE:DEFN:UserListBase. ■■ They must implement the method GetUsers. ■■ They must return an array of string (the array containing operator IDs). Let’s write a user list application class that returns members of the Portal Administrator role. For demonstration purposes, we will filter the results to even rows only. To get started, create a new application class in the application package APT_WA_AWE and name it WebAsset_ApprUserList. The code for this class follows: import PTAF_CORE:DEFN:UserListBase; class WebAsset_ApprUserList extends PTAF_CORE:Defn:UserListBase method WebAsset_ApprUserList(&rec_ As Record); method GetUsers(&aryPrevOpr_ As array of string, &thread_ As Record) Returns array of string; end-class; method WebAsset_ApprUserList /+ &rec_ as Record +/ %Super = create PTAF_CORE:DEFN:UserListBase(&rec_); end-method; method GetUsers /+ &aryPrevOpr_ as Array of String, +/ /+ &thread_ as Record +/
Chapter 3: Approval Workflow Engine
101
/+ Returns Array of String +/ /+ Extends/implements PTAF_CORE:DEFN:UserListBase.GetUsers +/ Local array of string &oprid_arr = CreateArrayRept("", 0); Local SQL &admin_sql = CreateSQL("SELECT ROLEUSER " | "FROM PSROLEUSER WHERE ROLENAME = 'Portal Administrator'"); Local string &oprid; Local number &counter = 1; While &admin_sql.Fetch(&oprid) If (Mod(&counter, 2) = 0) Then &oprid_arr.Push(&oprid); End-If; &counter = &counter + 1; End-While; Return &oprid_arr; end-method;
The WebAsset_ApprUserList constructor takes a record parameter. AWE will pass a pointer to the user list record definition at runtime. The record parameter is the PTAFUSER_LIST row containing the user list’s AWE metadata. Generally speaking, you don’t need to do anything with this record except pass it on to the superclass constructor. The GetUsers method parameters provide you with transactional context. The &aryPrevOpr_ parameter contains a list of all the user IDs that previously approved this transaction. The &thread_ parameter contains a reference to the cross-reference record, APT_WA_AWE_XREF, providing you with access to the transaction’s key values. With our user list application class created, we can log in to PeopleSoft online and configure a user list definition. Navigate to Set Up HRMS | Common Definitions | Approvals | Maintain User Lists and add a new value named APT_WebAsset_Approvers. Figure 3-3 provides the rest of the details. Note Each of the PeopleSoft applications uses slightly different navigation, but most of them place user lists under Set Up [application name] | Common Definitions | Approvals. As you will see later, the user list description will be displayed in the Approval Status Monitor. Therefore, it is important that the description be concise. Notice the Include Users as Input check box and Transaction Keys as Input check box in Figure 3-3. When specifying an application class as a user list, you do not need to select these check boxes, because the AWE framework will automatically include these values as parameters to the GetUers method. If you choose SQL or query for the user list type, these values are passed to the SQL statement as bind parameters. The user passed to the SQL definition or query is the operator ID of the previous approver (or the requester if the approval was just initiated). When adding bind variables to queries or SQL statements, specify transaction keys in the same order in which they occur in the header transaction record. Whether you choose application class, query, or SQL, the previous approver on the first step is always the user who initiated the transaction.
102
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-3. APT_WebAsset_Approvers user list definition
Note The PeopleSoft documentation recommends that you select either Include Users as Input or Transaction Keys as Input, not both.
Configuring the AWE Metadata Now that we have created the necessary supporting definitions, we can log in to the PeopleSoft application online and create our approval business process.
Registering the Transaction Navigate to Set Up HRMS | Common Definitions | Approvals | Register Transactions and add a new process ID named APT_WebAssets. Specify the description Web Asset Approval Transaction and the cross-reference record we created earlier named APT_WA_AWE_XREF.
Chapter 3: Approval Workflow Engine
103
Tip Many of the applications contain an AWE configuration shortcut collection. For example, in HRMS, navigate to Set Up HRMS | Common Definitions | Approvals | Approvals Setup Center for quick access to all online AWE configuration components. Expand the Default Approval Component group box to expose its fields. Enter APT_CUSTOM for the menu and APT_WEB_ASSETS for the component. Expand the Approval Event Handler Class group box. Set the root package ID to APT_WA_ AWE and the path to WebAssetAppr_EventHandler. Expand the Transaction Approval Levels group box to display its fields. For the level, choose Header. For this workflow, we want header-level approvals. If you were creating a workflow transaction for line-level approvals, you would choose Line for the level. In the Record (Table) Name field, enter APT_WEB_ASSETS. This is the name of our header record. After selecting a record name, the Level Record Key Field Label IDs grid will populate with the key fields from the chosen record. Select a label for each key field. Figure 3-4 shows the AWE process ID for this example.
FIGURE 3-4. APT_WebAssets AWE process ID
104
PeopleSoft PeopleTools Tips & Techniques
Configuring the Transaction Navigate to Set Up HRMS | Common Definitions | Approvals | Configure Transactions and select the APT_WebAssets process ID, which we created in the previous section. This is the step where we identify the notification message sent to each participant in the approval process and the event that triggers that notification. The only item required at level 0 in this component is an Approval User Info View value. An Approval User Info View is a standard view that contains the OPRID field, as well as fields providing information about a user. AWE uses this information to display approver information in the AWE status monitor. Many PeopleSoft applications deliver application-specific user info views. An HRMS user info view, for example, may display a user’s job title and manager. For our purposes, we can use the information provided by PSOPRDEFN_VW, a delivered view that derives a user’s name from that user’s security profile. In the Events scroll area, we need to add a row for each event this workflow will trigger. Configure the Final Approval, Final Denial, and Route for Approval events as shown in Figures 3-5, 3-6, and 3-7.
FIGURE 3-5. Transaction configuration for On Final Approval event
Chapter 3: Approval Workflow Engine
105
FIGURE 3-6. Transaction configuration for On Final Denial event Note For detailed information about the transaction events, see the “Approval Workflow Engine (AWE) for HCM 9.0” red paper available from Oracle’s support site.
Setting Up Process Definitions The next, and final, piece of AWE metadata to configure is the flow of approvals through AWE. This flow is called a process definition. A single process ID can contain multiple definitions. AWE provides two methods for differentiating between process definitions: ■■ Specify the definition ID when triggering the workflow process (hard-coded). ■■ Use definition criteria configured through the Setup Process Definitions page to determine which definition to apply.
106
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-7. Transaction configuration for Route for Approval event
While still logged in to PeopleSoft through your web browser, navigate to Set Up HRMS | Common Definitions | Approvals | Setup Process Definitions and select the Add a New Value tab. Select APT_WebAssets as the process ID and enter SHARE for the definition ID. Ignoring the Details and Criteria links for now, add a description for each level and a user list for step 1. This approval has only one path and one step, as shown in Figure 3-8. If you were setting up an approval with multiple paths and steps, you would add those here. For example, an expense report may require supervisor, director, and auditor approval steps. For the process definition, you can specify the following criteria: ■■ Definition criteria At runtime, if you don’t specify a specific definition, AWE will use the definition criteria to determine which process definition to apply. Providing definition criteria is not required. When we write PeopleCode to submit a transaction to AWE, you will see how to hard-code definition criteria. By using definition criteria, however, you can configure different approvals based on transactional and environmental conditions. For example, you may need to create separate process definitions for different countries or business units.
Chapter 3: Approval Workflow Engine
107
FIGURE 3-8. APT_WebAssets SHARE process definition
■■ Path criteria Some organizations use dollar limits to determine approval paths. For example, if your organization allows supervisors to approve expense reports under $50, requires manager approval for expense reports between $50 and $500, and requires director approval for anything over $500, you can configure those paths here by using path criteria to determine which path to apply. ■■ Step criteria You can use step criteria to determine whether or not to apply a particular step. For this simple workflow approval, we have no criteria and only one process definition, path, and step. So, for our example, change the Criteria Type setting in the Definition, Paths, and Steps areas to Always True. Figure 3-8 shows a check mark over the criteria icon for each of the process definition’s levels as an indicator that the definition, path, and step have criteria.
108
PeopleSoft PeopleTools Tips & Techniques
In addition to the Always True criteria type, AWE offers User Entered and Application Class types. The User Entered type provides a complex business rules editor that allows functional experts to configure criteria based on the state of the transaction. The Application Class type goes even further to provide unlimited criteria opportunities.
Modifying the Transaction Even though adding workflow functionality to a transaction can be quite invasive, don’t let this fact discourage you. When considering the maintenance cost of a modification versus the productivity benefit, be sure to include soft costs, such as how this modification will improve relations between your department and the rest of the organization. Information technology (IT) departments are often searching for ways to improve their reputation within an organization. Providing value-added system enhancements, such as workflow-enabling transactions, is a good place to start. Even though we are adding workflow functionality to a custom transaction, we will treat it as if it were a delivered transaction to demonstrate techniques for reducing the impact of workflow modifications on delivered transactions.
Adding Approval Fields to the Transaction By workflow-enabling the web asset transaction, we are saying that a web asset is not available for use until it is approved. Therefore, we will add an approval flag to the transaction to indicate the approval state of the transaction. Following the design pattern established in Chapter 2, we will create a sibling record to store the approval flag. Since we are adding the approval flag to the transaction’s header, level 0, we won’t have the “More than one data record” concern we experienced with the Driver’s License Data component in Chapter 2. Note As you recall from Chapter 2, we already have a sibling record for storing attachments. This is also a customization, and a suitable place to store the approval fields. For discussion purposes, we will add another sibling record to this transaction. We will store the approval flag in a new record definition named APT_WA_APPR. Create this new record definition and add two fields: ASSET_ID, as the key field, and APPR_STATUS, with the default constant value P, as shown in Figure 3-9. Save and build the record. Let’s add the approval status field to the page now. Open the APT_WEB_ASSETS page in App Designer and drag the APT_WA_APPR.APPR_STATUS field from the project workspace onto the page canvas, as shown in Figure 3-10.
Chapter 3: Approval Workflow Engine
109
FIGURE 3-9. APT_WA_APPR approval sibling record
After adding the approval status field to this page, set the field to display only. Later, we will add buttons to this page to change the transaction’s approval status. Also, shrink the Comment field to make room for the approval status. We will need to make additional layout changes to this page later when we add the Submit, Approve, and Deny buttons. Figure 3-11 shows the updated Web Assets page. Do you still have a test transaction from Chapter 2? If so, with your test Web Asset definition open, change a value in one of the fields and then save. If you have database access to your system, select all the rows from the database table PS_APT_WA_APPR. Notice that your test asset isn’t listed. In fact, if you haven’t added any new web assets since adding the APPR_STATUS field
110
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-10. APT_WEB_ASSETS page definition with the APPR_STATUS field
to the page, there won’t be any rows in the PS_APT_WA_APPR table. The component processor does not automatically insert new rows into APT_WA_APPR when saving existing transactions. It will, however, insert rows for new transactions. This is a bit misleading, because the page displays the APPR_STATUS default value of Pending. Therefore, when adding fields to a page in this manner, it is important to consider the impact on existing transactions.
Adding Approval Buttons Next, let’s add workflow buttons to this page. This page requires a Submit button for the transaction creator to initiate the workflow process, as well as Approve and Deny buttons for the approver. PeopleSoft delivers Submit, Approve, and Deny fields designed to be used as buttons, we just need to create a derived/work record to hold them.
Chapter 3: Approval Workflow Engine
111
FIGURE 3-11. APT_WEB_ASSETS page with APPR_STATUS field
Open App Designer and create a new record by choosing File | New from the App Designer menu bar. Change the record type to derived/work. Add the fields APPROVE_BTN, DENY_BTN, and SUBMIT_BTN. Save the record as APT_WA_APPR_WRK. Figure 3-12 shows this new APT_ WA_APPR_WRK record definition. Now add three standard buttons to the page, and then set the buttons’ record name to APT_WA_APPR_WRK and the field name to the appropriate field. Figure 3-13 is a design view of the page with the three approval buttons. Later, we will add display logic to hide the Approve and Deny buttons if a viewer is not an approver. Also, we will want to show the Submit button when the transaction is created, but not after the transaction has already been submitted.
112
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-12. APT_WA_APPR_WRK derived/work record
Note In Chapter 2, we created a custom file attachment button state function to avoid hiding user interface elements. Now I am specifically hiding buttons. What’s the difference? When using the file attachment buttons, the same user has access to all three buttons. With approvals, the submitting user should not be able to approve the transaction. Therefore, the Approve and Deny buttons are not user interface elements for the submitting user. Likewise, the user approving or denying the transaction is not the submitting user, and therefore should not have the Submit button. This differs from the file attachment example, in that the three buttons are for the file attachment user, whereas the Approve and Deny buttons are not for the submitting user.
Chapter 3: Approval Workflow Engine
113
FIGURE 3-13. Design view of the Web Assets page with approval buttons
Adding the Approval Status Monitor A new workflow feature made possible by the AWE framework is the Approval Status Monitor. We will cover several features of this monitor later in this chapter. Adding the Approval Status Monitor to a page is optional. To make room for the Approval Status Monitor, we will need to rearrange a few more fields. The Approval Status Monitor resides in the subpage PTAF_MON_SBP. To add the monitor to a page, choose Insert | Subpage from the App Designer menu. When prompted for a subpage name, choose PTAF_MON_SBP. Figure 3-14 shows the APT_WEB_ASSETS page after rearranging fields and adding the Approval Status Monitor subpage. When developing, I like to make small changes and view the results of those changes before moving on to the next change. It is much easier to find and resolve problems when making small, incremental changes, rather than after building an entire customization. Therefore, I encourage you to reload the Web Asset page in your browser after adding the Approval Status Monitor. Don’t expect to see anything other than layout changes. The Approval Status Monitor won’t display until we add initialization PeopleCode and submit a transaction.
114
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-14. Design view of the Web Assets page with the Approval Status Monitor
We can now write some display logic to update the user interface and business logic to submit, approve, and deny web asset transactions.
Component Buffer Utilities Users should be able to modify a web asset definition until that definition is submitted. Once submitted, the transaction should change to read-only. One method to disable fields at runtime is to drag each field from the App Designer project workspace outline onto the PeopleCode editor, and then set the Enabled property of that field to False, as follows: APT_WEB_ASSETS.DESCR.Enabled = False; APT_WEB_ASSETS.MIMETYPE.Enabled = False; ...
I have another technique. Rather than list each field individually, I have some shared FUNCLIB functions that iterate over the fields in a record or list of records and set the Enabled state of the field accordingly.
Chapter 3: Approval Workflow Engine
115
Create a new derived/work record named APT_FIELD_FUNC and insert the field FUNCLIB. Add the following code to the FieldFormula event of the FUNCLIB field. /* * Set the enabled state of all the fields in &rec to &state. You * don't have to call this function directly. For convenience and for * clarity, call the enable_fields or disable_fields function. */ Function set_fields_state(&rec As Record, &state As boolean) Local number &fieldIndex = 1; For &fieldIndex = 1 To &rec.FieldCount &rec.GetField(&fieldIndex).Enabled = &state; End-For; End-Function;
/* * Set the enabled state of all the fields in all the records in &recs * to &state. You don't have to call this function directly. For * convenience and for clarity, call the enable_records or * disable_records function. */ Function set_records_state(&recs As array of Record, &state As boolean) Local number &recIndex = 0; While &recs.Next(&recIndex) set_fields_state(&recs [&recIndex], &state); End-While; End-Function;
/* * Enable each field in each record in &recsToEnable */ Function enable_records(&recsToEnable As array of Record) set_records_state(&recsToEnable, False); End-Function;
/* * Disable each field in each record in &recsToDisable */ Function disable_records(&recsToDisable As array of Record) set_records_state(&recsToDisable, False); End-Function;
116
PeopleSoft PeopleTools Tips & Techniques
/* * Enable each field in &rec */ Function enable_fields(&rec As Record) set_fields_state(&rec, True); End-Function;
/* * Disable each field in &rec */ Function disable_fields(&rec As Record) set_fields_state(&rec, False); End-Function;
Of the functions listed in the preceding code, we will call only the disable_records function. The call stack for disable_records looks like this: disable_records set_records_state set_fields_state Since the Web Assets component is relatively small, it would be more efficient to write 5 RECORD.FIELD.Enabled = False statements than to write 60 lines of reusable functions. A component with only five data-entry fields and buttons, however, is the exception. Imagine writing RECORD.FIELD.Enabled = False statements for all of the fields in the Purchase Order component buffer! I like code that communicates clearly. The code’s purpose should be self-evident. Yes, I did take 60 lines in a FUNCLIB to do what I could have done in 5 lines. But, these 60 lines will take a lot of “noise” out of my event handler code. Rather than writing code like this: APT_WEB_ASSETS.ASSET_ID.Enabled = False APT_WEB_ASSETS.DESCR.Enabled = False; APT_WEB_ASSETS.MIMETYPE.Enabled = False; APT_WEB_ASSETS.COMMENTS.Enabled = False; FILE_ATTACH_WRK.ATTACHADD.Enabled = False; FILE_ATTACH_WRK.ATTACHDELETE.Enabled = False;
I can write code that looks like this: disable_records(CreateArray( GetRecord(Record.APT_WEB_ASSETS), GetRecord(Record.FILE_ATTACH_WRK)));
Tip By combining recursion with the preceding functions, it is possible to disable all the fields in a row and the row’s child rowsets regardless of the number of scroll levels.
Chapter 3: Approval Workflow Engine
117
The PostBuild PeopleCode AWE provides two top-level application classes for submitting and managing workflow instances: ■■ PTAF_CORE:LaunchManager contains properties and methods for working with transactions prior to submission. ■■ PTAF_CORE:ApprovalManager contains properties and methods for administering submitted transactions. We will use both application classes in this example. The AWE developers recommend creating the LaunchManager and ApprovalManager application classes in the component PostBuild event, and then storing those objects as component-scoped variables. Since PostBuild happens after the rest of the component events, we can also use PostBuild to make any final, AWE-related user interface changes. If you haven’t already done so, log into App Designer and open the APT_WEB_ASSETS component. Choose View | View PeopleCode from the App Designer menu. We will start with some declarations: /******** Declarations ********/ import PTAF_CORE:LaunchManager; import PTAF_CORE:ApprovalManager; Declare Function update_ui PeopleCode APT_WA_APPR_WRK.APPROVE_BTN FieldFormula;
/******** Component Scoped Variables ********/ REM ** Component scoped AWE Launch Manager for submitting; Component PTAF_CORE:LaunchManager &c_aweLaunchManager; REM ** Component scoped AWE Approval Manager for approve/deny; Component PTAF_CORE:ApprovalManager &c_aweApprManager;
Note I find it difficult to keep track of component- and global-scoped variables. To assist in differentiating local, component, and global variables, I’ve adopted the prefix c_ for component scope and g_ for global scope. Our transaction has three workflow action buttons: Submit, Approve, and Deny. Even though a button click triggers a change in the workflow state, we won’t update the workflow from the FieldChange event. Rather, we will wait until after save processing to submit, approve, or deny the transaction. It would be incorrect to submit a transaction that fails save processing validation (SaveEdit and deferred FieldEdit) because this would place an invalid transaction in the workflow system.
118
PeopleSoft PeopleTools Tips & Techniques Note Submitting transactions using the SavePostChange event is a recommendation that assumes the submitted transaction is loaded into the component buffer. If you write code that submits transactions to AWE using standalone records or transactions unrelated to the component buffer, it is not necessary to wait for SavePostChange.
We must keep track of the workflow button that was pressed so we can take the appropriate action in the SavePostChange event. We have three buttons and, therefore, three values to track. Since these values are mutually exclusive, we can use a single component-scoped variable to track the selected button. The following PeopleCode segment declares this component-scoped variable. We won’t use this variable in the PostBuild event, so it is not necessary to declare it here. I do this as a convention only. I find component and global variables difficult to follow because there are so many events that can use a single variable. /* * Possible approval actions (button click): * S - submit * A - approve * D - deny * * Store chosen action in a component variable. */ Component string &c_apprAction;
Earlier in this chapter, we created a process ID when we registered the Web Assets component with AWE. We must pass this value to the AWE LaunchManager and ApprovalManager class constructors to identify the appropriate workflow process. The following code creates a constant to hold the process ID: /******** Constants ********/ REM ** Process ID from Register Transactions; Constant &PROCESS_ID = "APT_WebAssets";
With declarations out of the way, we can initialize the LaunchManager and ApprovalManager and then update the user interface elements. /******** PostBuild mainline code ********/ REM ** pointer to transaction header; Local Record &headerRec = GetRecord(Record.APT_WEB_ASSETS);
/* Initialize the launch and approval managers. ApprovalManager will * need reinitialization on submit */ &c_aweLaunchManager = create PTAF_CORE:LaunchManager(&PROCESS_ID, &headerRec, %OperatorId); &c_aweApprManager = create PTAF_CORE:ApprovalManager(&PROCESS_ID, &headerRec, %OperatorId);
Chapter 3: Approval Workflow Engine
119
/* Uncomment the following line if you don't want AWE to choose the * Definition Id based on the preconfigured definition criteria. * Definition criteria is maintained using the "Setup Process * Definition" component. */ REM &c_aweLaunchManager.definition = "SHARE"; update_ui(GetLevel0(), &c_aweLaunchManager, &c_aweApprManager); REM ** Turn on tracing after Defn ID is set (by AWE or hard coded); REM &c_aweLaunchManager.appDef.trace_flag = True;
Note Both the ApprovalManager and LaunchManager constructors have an operator ID parameter. It would be possible for AWE to determine the operator ID from the %OperatorId system variable (in fact, AWE does store the operator ID of the requesting user). By making the operator ID a constructor parameter, it is possible for users to submit approvals on behalf of other users. In this example, we allow AWE to choose the appropriate process definition ID based on the criteria we configured in the Setup Process Definitions component. The LaunchManager class uses a technique called lazy initialization to initialize the process definition prior to using it. You can force AWE to select a process definition ID sooner by accessing the hasAppDef property. The preceding code listing calls a function named update_ui. The PostBuild and SavePostChange events will contain the same user interface logic. To avoid redundancies, we centralize the common user interface code in a function named update_ui, which we will create in the next section. The last line in the previous code listing is commented out. When executed, this line will cause AWE to trace the selection of process definition stages, paths, and steps. Since the AWE trace flag belongs to the LaunchManager class (appDef property), we must wait until one of the LaunchManager properties or methods selects the correct process definition. The update_ ui function forces the LaunchManager to select a process definition by accessing the submitEnabled property. Here is the complete listing of the component’s PostBuild PeopleCode: /******** Declarations ********/ import PTAF_CORE:LaunchManager; import PTAF_CORE:ApprovalManager; Declare Function update_ui PeopleCode APT_WA_APPR_WRK.APPROVE_BTN FieldFormula;
/******** Component Scoped Variables ********/ REM ** Component scoped AWE Launch Manager for submitting; Component PTAF_CORE:LaunchManager &c_aweLaunchManager;
120
PeopleSoft PeopleTools Tips & Techniques
REM ** Component scoped AWE Approval Manager for approve/deny; Component PTAF_CORE:ApprovalManager &c_aweApprManager; /* * Possible approval actions a user can take (button clicked): * S - submit * A - approve * D - deny * * Store chosen action in a component variable. */ Component string &c_apprAction;
/******** Constants ********/ REM ** Process ID from Register Transactions; Constant &PROCESS_ID = "APT_WebAssets"; /******** PostBuild mainline code ********/ REM ** pointer to transaction header; Local Record &headerRec = GetRecord(Record.APT_WEB_ASSETS);
/* Initialize the launch and approval managers. ApprovalManager will * need reinitialization on submit */ &c_aweLaunchManager = create PTAF_CORE:LaunchManager(&PROCESS_ID, &headerRec, %OperatorId); &c_aweApprManager = create PTAF_CORE:ApprovalManager(&PROCESS_ID, &headerRec, %OperatorId);
/* Uncomment following line if you don't want AWE to choose the * Definition Id based on the preconfigured definition criteria. * Definition criteria is maintained using the "Setup Process * Definition" component. */ REM &c_aweLaunchManager.definition = "SHARE"; update_ui(GetLevel0(), &c_aweLaunchManager, &c_aweApprManager); REM ** Turn on tracing after Defn ID is set (by AWE or hard coded); REM &c_aweLaunchManager.appDef.trace_flag = True;
Note The LaunchManager and ApprovalManager application classes form the external, top-level PeopleCode interface into AWE. For a complete listing of properties and methods, open the PTAF_CORE application package in App Designer, and then double-click the LaunchManager or ApprovalManager class to open it in a PeopleCode editor.
Chapter 3: Approval Workflow Engine
121
Common UI Code You won’t be able to save your PostBuild PeopleCode until you create the update_ui function, so now is a good time to create it. Add the following PeopleCode to the FieldFormula event of the APPROVE_BTN field in the APT_WA_APPR_WRK record. You can place shared functions in any event of any field attached to a record definition. By convention (it’s not a requirement), we place them in the FieldFormula event of the first field in a record definition. import PTAF_CORE:LaunchManager; import PTAF_CORE:ApprovalManager; Declare Function createStatusMonitor PeopleCode PTAFAW_MON_WRK.PTAFAW_FC_HANDLER FieldFormula; Declare Function disable_records PeopleCode APT_FIELD_FUNC.FUNCLIB FieldFormula; /* * Update the APT_WEB_ASSETS Page user interface based on the state * of the current workflow transaction. * * The parameter &rs0 is a rowset containing one row. That one row * must have the following records: * * Record.APT_WEB_ASSETS * Record.FILE_ATTACH_WRK * Record.APT_WA_APPR_WRK * * &rs0 represents Level 0 for component APT_WEB_ASSETS */ Function update_ui(&rs0 As Rowset, &launchManager As PTAF_CORE:LaunchManager, &apprManager As PTAF_CORE:ApprovalManager) Local Row &row1 = &rs0.GetRow(1); Local Record &asset_rec = &row1.GetRecord(Record.APT_WEB_ASSETS); Local Record &attach_wrk_rec = &row1.GetRecord(Record.FILE_ATTACH_WRK); Local Record &appr_wrk_rec = &row1.GetRecord(Record.APT_WA_APPR_WRK); Local boolean &isApprover = False; /* If the transaction was submitted to AWE then: * Create the status monitor * Disable transaction fields */ If (&apprManager.hasAppInst) Then &isApprover = &apprManager.hasPending; REM ** Initialize the AWE status monitor; createStatusMonitor(&apprManager.the_inst, "D", Null, False);
122
PeopleSoft PeopleTools Tips & Techniques REM ** Disable fields since transaction was submitted; disable_records(CreateArray(&asset_rec, &attach_wrk_rec)); REM ** Reenable the View button; &attach_wrk_rec.ATTACHVIEW.Enabled = True; End-If;
REM ** Set the state of buttons based on the state of the AWE Trx; &appr_wrk_rec.SUBMIT_BTN.Visible = &launchManager.submitEnabled; &appr_wrk_rec.APPROVE_BTN.Visible = &isApprover; &appr_wrk_rec.DENY_BTN.Visible = &isApprover; End-Function;
The update_ui function uses an interesting pattern. The first parameter to this function is a rowset named &rs0. The zero in the name refers to level 0, which happens to be a rowset with one row. Throughout this book, I’m stressing the importance of writing code that is independent of its execution context. It is very difficult—sometimes even impossible—to unit test code that depends on the component processor to provide context. As we move into Parts II and III of this book, my reasons for stressing this fact should become evident. The preceding code disables the component’s data-entry fields and file attachment buttons. Once this transaction is submitted to the workflow framework, we don’t want users to modify the attachment. We do, however, want users to be able to view the attachment. Therefore, after calling the generic field disabling routine, we must reenable the View button. In Chapter 2, we wrote code in the RowInit event to enable or disable the file attachment buttons based on the state of the attachment field. By calling this shared function in the PostBuild event, we are actually overriding that RowInit PeopleCode. This happens because the PostBuild event executes after the RowInit event. Since everything in this component happens at level 0, and level 0 is guaranteed to have only one row, we could have written this code in RowInit and enabled only the file attachment buttons if the transaction didn’t have an AWE transaction. For a multilevel component, however, that may not be an option because all of the data needed to initialize the workflow engine may not exist until all the rows are loaded. In PostBuild, with recursion and loops, we can disable every field at every level. Using RowInit, we would need to modify every record’s RowInit event.
Enabling the Workflow Buttons LaunchManager provides the DoSubmit method to submit transactions to the workflow engine. ApprovalManager has DoApprove and DoDeny methods for approving or denying transactions. We won’t call these methods directly from FieldChange PeopleCode. Rather, we want the component processor to execute all component edits and save logic prior to submitting this transaction to the workflow engine. This ensures that we submit only saved transactions to the workflow engine. Consider the scenario where a user creates a transaction, clicks a submit button, but then exits the transaction without saving, discarding the transaction as if it never happened. The workflow engine would now have a submitted workflow process with no corresponding transaction. To avoid this, we will use the &c_apprAction component variable we declared in the PostBuild event. The FieldChange event will update this variable and then call the DoSave PeopleCode function to execute save processing.
Chapter 3: Approval Workflow Engine
123
The final event in save processing is SavePostChange. We will call the appropriate workflow method from the SavePostChange event, using the &c_apprAction component variable to determine which method to call. To add FieldChange PeopleCode to the Submit button, log in to App Designer and open the APT_WEB_ASSETS component. Choose View | View PeopleCode from the App Designer menu bar. In the PeopleCode editor, change the value in the upper-left drop-down list to the SUBMIT_BTN field of the APT_WA_APPR_WRK record. Change the value in the upper-right drop-down list to FieldChange and enter the following PeopleCode: Component string &c_apprAction; &c_apprAction = "S"; If ( Not GetRow().IsChanged) Then REM ** force save processing; SetComponentChanged(); End-If; DoSave();
This FieldChange PeopleCode doesn’t change any transaction data. Therefore, without a call to SetComponentChanged, the event won’t trigger save processing. If save processing doesn’t happen, our SavePostChange PeopleCode won’t fire. We wrapped the call in an If statement so that we don’t call SetComponentChanged if the component is already marked as changed. Our APPROVE_BTN and DENY_BTN PeopleCode look similar. Switch to the APPROVE_BTN FieldChange event and add the following PeopleCode: Component string &c_apprAction; &c_apprAction = "A"; If ( Not GetRow().IsChanged) Then REM ** force save processing; SetComponentChanged(); End-If; DoSave();
In the DENY_BTN FieldChange event, add the following: Component string &c_apprAction; &c_apprAction = "D"; If ( Not GetRow().IsChanged) Then REM ** force save processing; SetComponentChanged(); End-If; DoSave();
The only difference between these three events is the value for &c_apprAction.
124
PeopleSoft PeopleTools Tips & Techniques
To add PeopleCode to the component SavePostChange event, with the component PeopleCode editor still open, change the value in the upper-left drop-down list to APT_WEB_ ASSETS.GBL. Change the value in the upper-right drop-down list to SavePostChange and add the following PeopleCode: import PTAF_CORE:LaunchManager; import PTAF_CORE:ApprovalManager; Declare Function update_ui PeopleCode APT_WA_APPR_WRK.APPROVE_BTN FieldFormula; Component string &c_apprAction; Component PTAF_CORE:LaunchManager &c_aweLaunchManager; Component PTAF_CORE:ApprovalManager &c_aweApprManager; Local Record &headerRec = GetRecord(Record.APT_WEB_ASSETS); Local boolean &isApprover; Evaluate &c_apprAction When "S" &c_aweLaunchManager.DoSubmit(); If (&c_aweLaunchManager.hasAppInst) Then REM ** Initialize Approval Manager if transaction was submitted; &c_aweApprManager = create PTAF_CORE:ApprovalManager( &c_aweLaunchManager.txn.awprcs_id, &headerRec, %OperatorId); End-If; Break; When "A" &c_aweApprManager.DoApprove(&headerRec); Break; When "D" &c_aweApprManager.DoDeny(&headerRec); Break; End-Evaluate; update_ui(GetLevel0(), &c_aweLaunchManager, &c_aweApprManager); /* tracing options */ REM Local File &trace = GetFile("/tmp/apt_awe_trace.txt", "A", "A", %FilePath_Absolute); REM &trace.WriteLine(&c_aweLaunchManager.appDef.trace); REM &trace.Close();
This SavePostChange PeopleCode uses the LaunchManager and ApprovalManager that we initialized in the PostBuild event. The evaluate statement determines which manager to call based on the button that was pressed. The commented code at the end of this event corresponds to the PostBuild tracing code you saw earlier. If you turned on tracing in PostBuild, use SavePostChange to write the trace value to a text file.
Chapter 3: Approval Workflow Engine
125
Tracing AWE Since AWE is implemented entirely with PeopleCode, you can use the PeopleCode debugger and PeopleCode trace settings to debug and trace an approval as it moves through AWE. If your primary interest is in seeing how AWE applies criteria to choose stages, paths, and steps, you can turn on tracing in the PTAF_CORE:DEFN:AppDef class. For example, in our PostBuild code, we could turn on tracing by adding the following statement directly after setting the definition ID: &c_aweLaunchManager.appDef.trace_flag = True;
To save this trace to a file, add PeopleCode similar to the following at the end of the SavePostChange event: Local File &trace = GetFile("/tmp/awe_trace.txt", "A", "A", %FilePath_ Absolute); &trace.WriteLine(&c_aweLaunchManager.appDef.trace); &trace.Close();
The following trace file shows that AWE was not able to find a process definition path for the current transaction: Instantiating [Process definition: 'APT_WebAssets', Definition ID: 'SHARE', Eff date: '2009-07-04'] Header=(ASSET_ID=TEST9;) Found 1 stages. Instantiating stage 1: level = 0, descr = Web Asset Approval Found 1 paths. Defined Path 1: criteria check = False **** Skipping path 1 **** Skipping stage 10
If your approver user list contains flawed logic, then you may see a trace that looks similar to this: Instantiating [Process definition: 'APT_WebAssets', Definition ID: 'SHARE', Eff date: '2009-07-04'] Header=(ASSET_ID=TEXT_FILE;) Found 1 stages. Instantiating stage 1: level = 0, descr = Web Asset Approval Found 1 paths. Defined Path 1: criteria check = True Found 1 steps. Step 1: criteria check=True Step instance 5135, step number 1
126
PeopleSoft PeopleTools Tips & Techniques
Prev approvers (HCRUSA_KU0001) Approvers () Need 1 approvers, but found 0, requiring next Next (another) step is required (too few approvers in prev step), but not found: inserting error step!
The following trace file shows a web asset transaction without errors. You can see that AWE was able to find one path, one step, and two approvers: Instantiating [Process definition: 'APT_WebAssets', Definition ID: 'SHARE', Eff date: '2009-07-04'] Header=(ASSET_ID=TEXT_TEST;) Found 1 stages. Instantiating stage 1: level = 0, descr = Web Asset Approval Found 1 paths. Defined Path 1: criteria check = True Found 1 steps. Step 1: criteria check=True Step instance 5145, step number 1 Prev approvers (HCRUSA_KU0001) Approvers (HCRUSA_KU0012,PSEM)
Tip Use one of the clipboard copy commands (such as ctrl-c) to copy component-scoped variables from one PeopleCode event to another. The PeopleCode validator does not check for inconsistencies between component-scoped variable names. It assumes that two variables with different names are actually two different declarations.
Testing the Approval We have enough code to submit a test transaction to AWE. If you test with a transaction you created in Chapter 2, be sure to add a corresponding row to the new APT_WA_APPR record. Prior to testing the Submit button, I ran the following SQL to add my Chapter 2 TEST web asset to the new APT_WA_APPR record: INSERT INTO PS_APT_WA_APPR VALUES('TEST', 'P') / COMMIT /
Note Prior to testing the transaction’s PeopleCode, you can verify your AWE configuration using the Preview Approval Process link on the AWE Process Definition configuration page.
Chapter 3: Approval Workflow Engine
127
In Chapter 2, after creating the Web Asset component, we created a role and permission list to provide access to this new component. We also added this role to a user profile so we could access and test the component. We can use that same user to add a new web asset and submit it for approval. Before submitting a transaction, however, we need an approver. When we created the user list, we wrote code to select even-numbered Portal Administrators. Before testing this new workflow, make sure you have at least two Portal Administrators that are also members of the APT_CUSTOM role. Once you have a submitter and some approvers, create a new web asset. Prior to submitting a web asset, the Description, Mime-Type, and Comment fields should be enabled, and the Submit button should be visible. Once you submit it, the Submit button should disappear, the data-entry fields should change to disabled, and the Approval Status Monitor should appear. Figure 3-15 shows one of my many test web assets after submission. Workflow-enabling a transaction requires a few new App Designer definitions, some PeopleCode, and some metadata configuration. Don’t be discouraged if your first few tests fail. After Chapter 2, I had one web asset. By the end of this chapter, I had 38. It appears that it took me 37 tries to get it right!
FIGURE 3-15. Web asset after submission
128
PeopleSoft PeopleTools Tips & Techniques Tip The Submit button is visible if the LaunchManager.submitEnabled property returns the value True. This property returns True only if the process ID and definition ID exist and the transaction wasn’t previously submitted. Therefore, if your Submit button is invisible, compare your process ID with the process ID in the Register Transactions component. Also check your process definition criteria. It is possible that the LaunchManager was not able to find a definition matching your criteria. If you suspect your definition criteria is the problem, then try hard-coding the LaunchManager.definition property to your definition ID as shown in the component’s PostBuild PeopleCode.
In my HRMS demo database, I submitted the web asset as user HCRUSA_KU0001. Two of the Portal Administrators, PSEM and HCRUSA_KU0012, received a notification e-mail, as shown in Figure 3-16. From this e-mail message, I clicked the hyperlink to go directly to the web asset transaction, logged in as user HCRUSA_KU0012, and approved the transaction. Figure 3-17 shows this asset after approval.
FIGURE 3-16. Web asset approval e-mail message
Chapter 3: Approval Workflow Engine
129
FIGURE 3-17. Approved web asset
Providing Custom Descriptions for the Approval Status Monitor
The text of the Approval Status Monitor is composed of descriptions and transaction key values. The Approval Status Monitor title comes from the process definition description, “Web Asset Approval.” The group box header is composed of the transaction header key field names and values, ASSET_ID=SUBMIT_TEST. The text under the approver’s name, “Web Asset Approvers,” comes from the user list description. AWE provides a mechanism to override the group box header and the approver’s name in the Approval Status monitor. We do this by creating an application class that extends the class PTAF_MONITOR:MONITOR:threadDescrBase. Since threadDescrBase provides a default implementation for each of its methods, you only need to override the method representing the text you want to change. Besides text in the Approval Status Monitor, threadDescrBase contains a method that allows you to override the text displayed in the approver’s worklist. The following code listing contains a sample implementation for each of the three threadDescrBase methods.
130
PeopleSoft PeopleTools Tips & Techniques
Add the class WebAsset_ThreadDescr to the application package APT_WA_AWE, and then insert the following code into the WebAsset_ThreadDescr class. import PTAF_MONITOR:MONITOR:threadDescrBase; class WebAsset_ThreadDescr extends PTAF_MONITOR:MONITOR:threadDescrBase method getThreadDescr(&keys As array of Field) Returns string; method getWorklistDescr(&recApplication As Record) Returns string; method getUserName(&OprId As string) Returns string; end-class; /* Set the group box header */ method getThreadDescr /+ &keys as Array of Field +/ /+ Returns String +/ /+ Extends/implements PTAF_MONITOR:MONITOR:threadDescrBase.getThreadDescr +/ Local Field &field = &keys [1]; Return &field.GetShortLabel(&field.Name) | ": " | &field.Value; end-method; /* Set the worklist transaction link description */ method getWorklistDescr /+ &recApplication as Record +/ /+ Returns String +/ /+ Extends/implements PTAF_MONITOR:MONITOR:threadDescrBase.getWorklistDescr +/ &recApplication.SelectByKey(); Return &recApplication.DESCR.Value; end-method; /* Provide a name for the approver */ method getUserName /+ &OprId as String +/ /+ Returns String +/ /+ Extends/implements PTAF_MONITOR:MONITOR:threadDescrBase.getUserName +/ Local string &name = %Super.getUserName(&OprId); If (Left(&name, 4) = "[PS]") Then &name = Substitute(&name, "[PS] ", ""); End-If; Return &name; end-method;
Chapter 3: Approval Workflow Engine
131
The username in Figure 3-17 is [PS] Allan Martin - EE. This name comes from the user’s security user profile and is the name given to user ID HCRUSA_KU0012. To make the name display a little friendlier, the getUserName method trims the [PS] portion from Allan’s name. We configure AWE to use WebAsset_ThreadDescr by updating the AWE process registration using the navigation Set Up HRMS | Common Definitions | Approvals | Register Transactions. Figure 3-18 shows the Approval Status Monitor settings, as well as an ad hoc package and class, which we will discuss in the next section. Figure 3-19 shows an approval using the new WebAsset_ThreadDescr application class. Notice the group box header changed from ASSET_ID=TESTWL to Asset ID: TESTWL. Also, the approver’s name no longer contains [PS].
FIGURE 3-18. Registration of a custom Thread class
132
PeopleSoft PeopleTools Tips & Techniques
FIGURE 3-19. Approval that uses the custom WebAsset_ThreadDescr class
Allowing Ad Hoc Access
When designing a workflow, you will map out the approval path to the best of your ability. Nevertheless, you may find that you need additional approvers. Consider the example of a human resources employee who works primarily with the engineering department. If that employee requests to transfer out of the human resources department, the human resources manager may want to add the engineering manager to an approval path prior to approving this request. Figure 3-20 shows such an ad hoc approval process. Here, I added Charles Baran as a reviewer and the demo superuser, PS, as the final approver. To enable ad hoc approvals, open the shared user interface PeopleCode we created earlier. You will find this code in the FieldFormula event of the APT_WA_APPR_WRK.APPROVE_BTN field. Locate the createStatusMonitor line that looks like this: createStatusMonitor(&apprManager.the_inst, "D", Null, False);
Chapter 3: Approval Workflow Engine
133
FIGURE 3-20. Ad hoc approval process
Replace the "D" with an "A": createStatusMonitor(&apprManager.the_inst, "A", Null, False);
The "D" means display only, and the "A" is for ad hoc. AWE contains default business logic to determine who can insert ad hoc approvers and ad hoc paths. As shown in Figure 3-18, I created a custom ad hoc approver application class to limit the number of ad hoc approvers. Ad hoc approver application classes extend the AWE-delivered application class PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase. The following code listing contains a basic ad hoc approver application class: import PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase; import PTAF_CORE:ENGINE:StepInst; import PTAF_CORE:ENGINE:StageInst; class WebAsset_AdhocAccess extends PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase
134
PeopleSoft PeopleTools Tips & Techniques method allowInsert(&oprid As string, &stepBefore As PTAF_CORE:ENGINE:StepInst, &stepAfter As PTAF_CORE:ENGINE:StepInst) Returns boolean; method allowDelete(&oprid As string, ¤tStep As PTAF_CORE:ENGINE:StepInst) Returns boolean; method allowNewPath(&oprid As string, &stage As PTAF_CORE:ENGINE:StageInst) Returns boolean;
private method isPortalAdmin(&oprid As string) Returns boolean; end-class; method allowInsert /+ &oprid as String, +/ /+ &stepBefore as PTAF_CORE:ENGINE:StepInst, +/ /+ &stepAfter as PTAF_CORE:ENGINE:StepInst +/ /+ Returns Boolean +/ /+ Extends/implements PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase.allowInsert +/ Return %This.isPortalAdmin(&oprid); end-method; method allowDelete /+ &oprid as String, +/ /+ ¤tStep as PTAF_CORE:ENGINE:StepInst +/ /+ Returns Boolean +/ /+ Extends/implements PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase.allowDelete +/ If (%Super.allowDelete(&oprid, ¤tStep)) Then Return True; Else Return %This.isPortalAdmin(&oprid); End-If; end-method; method allowNewPath /+ &oprid as String, +/ /+ &stage as PTAF_CORE:ENGINE:StageInst +/ /+ Returns Boolean +/ /+ Extends/implements PTAF_MONITOR:ADHOC_OBJECTS:adhocAccessLogicBase.allowNewPath +/ Return %This.isPortalAdmin(&oprid); end-method;
Chapter 3: Approval Workflow Engine
135
method isPortalAdmin /+ &oprid as String +/ /+ Returns Boolean +/ Local string &is_admin; SQLExec("SELECT ROLENAME FROM PSROLEUSER WHERE ROLEUSER = :1 AND" | " ROLENAME='Portal Administrator'", &oprid, &is_admin); Return (All(&is_admin)); end-method;
The allowInsert and allowNewPath methods in this listing return True if the user identified by parameter &oprid is a member of the Portal Administrator role. The allowDelete method possesses a little more logic. The base class implementation for allowDelete permits deletions if the user identified by &oprid added the approval step, or if the user is an administrative user. The allowDelete override in the preceding code retains the default logic by calling the base class’s allowDelete method and returning the same result. This example adds to the default logic by allowing Portal Administrators to delete paths and steps. Note The AdhocAccess class shown here uses SQL to determine whether a user is in a specific role. PeopleCode provides the IsUserInRole function for this purpose. Using IsUserInRole would have bound the WebAsset_AdhocAccess class to its execution context. Many of the approval framework methods contain an OPRID parameter. Use this parameter rather than assuming the approval is for the ID of the current user.
Workflow for Notifications Workflow isn’t just for approvals. Several years ago, I was working on my first PeopleTools 8.42 implementation. My team wrote a few App Engine batch process integrations to import data from external systems. If one of those batch processes failed, we wanted to notify an administrator who could resolve the issue and restart the integration process. The easiest way to implement notifications from an App Engine program is to use the SendMail PeopleCode function. While it is true that this function is simple, it requires the developer to hard-code information about the recipient. To take advantage of features like workflow’s alternate user ID, we decided to use the PeopleTools legacy workflow engine. The legacy workflow engine, unfortunately, is heavily dependent on the component processor. Therefore, to use the legacy workflow engine from a batch process, we needed to create a transaction record, page, and component, and then wrap that component in a component interface. As you have seen from this chapter, with AWE it is possible to submit and process a workflow transaction without the component buffer.
136
PeopleSoft PeopleTools Tips & Techniques
Creating an Event Handler Iterator
PeopleSoft applications deliver many transactions with workflow enabled. As an example, suppose that we have PeopleSoft Time and Labor and a non-PeopleSoft scheduling system, such as Oracle Workforce Scheduling. A scheduling system requires information about employees, including preferred working hours and absence requests. Using the OnFinalApproval workflow event, we could send approved absence information to a scheduling system. Looking at the AWE process IDs in the Register Transactions component, we see that the absence request process ID is AbsenceManagement. Opening that process ID, we see that the event handler is GP_ABS_EVT_HANDLER:apprEventHandler. To send an approved absence request to the scheduling program, we could modify the OnHeaderApprove event handler. As an alternative, we could create another handler, register it in AWE, and then call the delivered handler from the custom handler, effectively chaining handler implementations. Building upon the object-oriented concepts discussed in Chapter 1, we will create a single, configurable handler that calls a collection of handlers. This approach requires some custom metadata tables and a few lines of code. The metadata tables will contain a list of event handlers for a given process. The custom event handler will iterate over that list, calling the appropriate event handler method. Figure 3-21 shows the header and detail metadata records. I designed the metadata repository with online configuration in mind. I’ll let you design the online page and component (define search keys as appropriate for your component). The header record, APT_EVT_HNDLR, contains the AWE process ID as the primary key. The detail record, APT_EVT_HND_DTL, uses the AWE process ID and APP_CLASS as key fields. I included a sequence number in the detail table to ensure that event handlers fire in the appropriate order. After creating the metadata tables, create an application package named APT_AWE and add an application class named EventHandlerIterator. Add the following code to this new application class: import import import import import import
PTAF_CORE:ApprovalEventHandler; PTAF_CORE:ENGINE:AdHocStepInst; PTAF_CORE:ENGINE:AppInst; PTAF_CORE:ENGINE:StepInst; PTAF_CORE:ENGINE:UserStepInst; PTAF_CORE:ENGINE:Thread;
class EventHandlerIterator extends PTAF_CORE:ApprovalEventHandler method OnProcessLaunch(&appInst As PTAF_CORE:ENGINE:AppInst); method OnHeaderApprove(&appinst As PTAF_CORE:ENGINE:AppInst); method OnHeaderDeny(&userinst As PTAF_CORE:ENGINE:UserStepInst); private method callHandlers(&awprcs_id As string, &methodName As string, &parms As array of any); end-class; method OnProcessLaunch /+ &appInst as PTAF_CORE:ENGINE:AppInst +/ /+ Extends/implements PTAF_CORE:ApprovalEventHandler.OnProcessLaunch +/
Chapter 3: Approval Workflow Engine
FIGURE 3-21. Event handler iterator metadata repository
%This.callHandlers(&appInst.appDef.awprcs_id, "OnProcessLaunch", CreateArrayAny(&appInst)); end-method; method OnHeaderApprove /+ &appinst as PTAF_CORE:ENGINE:AppInst +/ /+ Extends/implements PTAF_CORE:ApprovalEventHandler.OnHeaderApprove +/ %This.callHandlers(&appinst.appDef.awprcs_id, "OnHeaderApprove", CreateArrayAny(&appinst)); end-method; method OnHeaderDeny /+ &userinst as PTAF_CORE:ENGINE:UserStepInst +/ /+ Extends/implements PTAF_CORE:ApprovalEventHandler.OnHeaderDeny +/
137
138
PeopleSoft PeopleTools Tips & Techniques %This.callHandlers(&userinst.thread.txn.awprcs_id, "OnHeaderDeny", CreateArrayAny(&userinst));
end-method; method callHandlers /+ &awprcs_id as String, +/ /+ &methodName as String, +/ /+ &parms as Array of Any +/ Local SQL &handler_sql = CreateSQL("SELECT APP_CLASS FROM " | "PS_APT_EVT_HND_DTL WHERE PTAFPRCS_ID = :1 ORDER BY SEQ_NO", &awprcs_id); Local string &appClassName; While &handler_sql.Fetch(&appClassName); ObjectDoMethodArray(CreateObject(&appClassName), &methodName, &parms) End-While; end-method;
Note The callHandlers method in the preceding code uses one of the PeopleCode generic object methods to execute an object’s method by name: ObjectDoMethodArray. AWE offers several opportunities to apply patterns like this. For example, you could handle application class definition criteria in a similar manner. Unlike the event handler example that processed every application class in the list, a definition criteria handler would return as soon as one of the definition criteria classes returned false. Using techniques similar to the EventHandlerIterator example described here, it is possible to add functionality to delivered applications without modifying delivered code.
Web Service-Enabling Approvals
One of the major benefits of AWE over PeopleSoft’s legacy workflow engine is the ability to web service-enable approvals. The legacy workflow engine supported web services, but only through component interfaces. With AWE, you can submit, approve, deny, or push back a workflow transaction without a component interface. Since legacy workflow required a component interface, web service-enabling approvals required a separate service operation handler for each approval process. Using AWE, you can write one service operation handler and pass the AWE process ID as part of the incoming message. As you saw when we tested the web assets approval, the notification process generates a link to the target approval page to include in e-mail notifications. When submitting transactions online through your web browser, AWE is able to generate the correct transaction link using the PeopleCode %Portal and %Node system variables. Unfortunately, these system variables apply only to online PeopleCode. As a fallback method, AWE uses the EMP_SERVLET URL definition.
Chapter 3: Approval Workflow Engine
139
Set the URL value to the portion of your application’s URL that includes the site name. In my case, that is http://hrms.example.com/psp/hrms. The following is a PeopleCode fragment describing what a service operation handler might look like: import PS_PT:Integration:IRequestHandler; import PTAF_CORE:ApprovalManager; class ApprovalHandler implements PS_PT:Integration:IRequestHandler method OnRequest(&MSG As Message) Returns Message; method OnError(&MSG As Message) Returns string; end-class; method OnRequest /+ &MSG as Message +/ /+ Returns Message +/ /+ Extends/implements PS_PT:Integration:IRequestHandler.OnRequest +/ Local Message &resultMsg; Local Record &headerRec; Local PTAF_CORE:ApprovalManager &apprManager; Local string &processId; /* * TODO * 1. * * 3. * 2. */
Authenticate the user (PS_TOKEN, WS-Security, etc) by calling SwitchUser Extract Process ID from &MSG Extract keys from &MSG to populate &headerRec
&apprManager = create PTAF_CORE:ApprovalManager( &processId, &headerRec, %OperatorId); If (&apprManager.hasAppInst) Then &apprManager.DoApprove(&headerRec); Else REM ** return error message; End-If; /* * TODO: Populate result message */ Return &resultMsg; end-method; method OnError /+ &MSG as Message +/
140
PeopleSoft PeopleTools Tips & Techniques
/+ Returns String +/ /+ Extends/implements PS_PT:Integration:IRequestHandler.OnError +/ Return "Error"; end-method;
When using web services, third-party systems can participate in the PeopleSoft workflow process.
Conclusion
In describing how to use AWE, this chapter made extensive use of concepts introduced in Chapter 1. The next chapter will continue this trend by showing how to use custom application classes to enhance the Pagelet Wizard.
ChaPter
4
Pagelet Wizard
142
PeopleSoft PeopleTools Tips & Techniques
A
s you work with PeopleSoft applications, you will notice the product’s emphasis on configuration. ChartField configuration is an excellent example of the PeopleSoft development team’s efforts to provide configuration opportunities. Besides reducing the number of modifications, configuration reduces the application users’ dependence on their development team. Online configuration tools allow users to accomplish tasks that would normally require custom development. The Pagelet Wizard is one of these tools.
Pagelets Defined
Pagelets are modular, display-only, self-contained page fragments. Pagelets are most often associated with home pages, but they also can be inserted into standard pages. Just like other content displayed in PeopleSoft, pagelets must be registered in the portal registry in a subfolder of Portal Objects | Pagelets. Since pagelets are content references, you can create them from components, iScripts, or external URLs. The Pagelet Wizard is a configuration tool that allows functional experts to create and share pagelets with various PeopleSoft users in their organization. In fact, since pagelets generated by the Pagelet Wizard support Web Services for Remote Portlets (WSRP), users of other portal products, such as Oracle WebCenter, can incorporate PeopleSoft pagelets into their third-party home pages. A pagelet can be composed of anything a web browser can render, from standard HTML to rich AJAX, Flash, Scalable Vector Graphics (SVG), or JavaFX. Chapters 5, 6, and 7 will show you how to integrate rich Internet technologies with PeopleTools. And if you apply the concepts presented in Chapters 5 through 7 to the Pagelet Wizard, your users can create their own rich user experience. I described pagelets as display-only because pagelets must not execute an event that causes the browser to submit the home page to the component processor. The component processor uses this submission technique, known as a postback, to maintain state between the online page and the component buffer. When a postback occurs, the component processor will transfer users from their home page to the component. In Chapter 7, you will learn how to use AJAX to overcome this limitation. Note PeopleTools 8.50 and higher use AJAX to reconcile differences between the component buffer and an online page, eliminating this postback issue. Nevertheless, I recommend that you use transaction pages to maintain data, and use pagelets to provide an alternative view of those transactions. Pagelets can dramatically improve a user’s experience because they bring relevant, usercentric transactional information to the top level of the application. For example, an accounts payable clerk must know the status of outstanding bills, as well as the amount of funds available to pay those bills. The clerk may want to pay some accounts within the discount period, but hold other bills until five days prior to their due date. Rather than requiring this clerk to sift through transactions, we can improve his experience by creating pagelets that list payables due in five days and payables due within the discount period. Since pagelets are composed of HTML, these lists can link directly to the PeopleSoft transactions they represent.
Chapter 4: Pagelet Wizard
143
Creating a Pagelet
Figure 4-1 is an example of a home page with several pagelets. We are going to use the Pagelet Wizard to create the Salaries by Department pagelet shown in the center of Figure 4-1. For our demonstration pagelet, we will create a pie chart pagelet from the query named DEPT_SALARIES__NVISION_, which is delivered with the HRMS demo database. Navigate to PeopleTools | Portal | Pagelet Wizard | Pagelet Wizard and add the new value APT_DEPT_ SALARIES. After you add a new value, the Pagelet Wizard guides you through the steps required to create a pagelet, as shown in Figures 4-2 through 4-7: ■■ Specify Pagelet Information Step 1 prompts for a title (required) and a description (optional). PeopleSoft will display the pagelet’s title on home pages. ■■ Select Data Source In step 2, you select a data type. For this pagelet, choose the PS Query type. After choosing a data type, the Pagelet Wizard will prompt you for the data type’s settings. Since we chose PS Query as the type, the Pagelet Wizard prompts for the name of the query. When prompted, provide the name DEPT_SALARIES__NVISION_.
FIGURE 4-1. Home page with pagelets
144
PeopleSoft PeopleTools Tips & Techniques
FIGURE 4-2. Pagelet Wizard step 1
FIGURE 4-3. Pagelet Wizard step 2
Chapter 4: Pagelet Wizard
FIGURE 4-4. Pagelet Wizard step 3
FIGURE 4-5. Pagelet Wizard step 4
145
146
PeopleSoft PeopleTools Tips & Techniques
FIGURE 4-6. Pagelet Wizard step 5
Note If the DEPT_SALARIES__NVISION_ query is not available to you, then you may not have the query’s base tables in your query security tree. I created this pagelet as a user with the PeopleSoft Administrator role, which gave me access to all records that exist in the various query security trees. ■■ Specify Data Source Parameters Step 3 is enabled only for data types with parameters. Some data types, such as HTML, have only settings. Others, such as PS Query, have settings and parameters. Since the data type we selected in step 2 has parameters, step 3 prompts us for values. You can hard-code a value for a prompt, specify that the value is derived from a system variable, or let the user enter an appropriate value. In my HRMS demo database, the DEPT_SALARIES__NVISION_ query returns about 2,300 rows, consisting of roughly 100 departments. If you were creating your own query for this pagelet, you would probably show the top 9 and lump the rest into a category titled “Other.” To keep things simple, we will just specify the value 10 for the maximum number of rows and a usage type of Fixed.
Chapter 4: Pagelet Wizard
147
FIGURE 4-7. Pagelet Wizard step 6
■■ Select Display Format In step 4, you choose a display format. Each data type has a predefined list of display formats. Choose the Chart display format for this example. ■■ Specify Display Options Step 5 allows you to configure a pagelet’s display by providing values for the pagelet’s display format. For example, the Chart display format requires you to declare the query field that contains data values. ■■ Specify Publishing Options Step 6 allows you to publish a pagelet as a home page pagelet, an Intelligent Context pagelet (template pagelets are Intelligent Context pagelets), or an embeddable transaction pagelet. The Pagelet Wizard offers the easiest mechanism for creating a pagelet. As this example demonstrates, the Pagelet Wizard is a tool designed for functional experts. It doesn’t require any coding or access to App Designer. If you are new to the Pagelet Wizard, I encourage you to spend some time investigating its features before moving on to the next section of this chapter. After creating a few pagelets yourself, you will be ready to move to the next step: extending the Pagelet Wizard by creating new data types, transformers, and display formats.
148
PeopleSoft PeopleTools Tips & Techniques
Components of a Pagelet Wizard Pagelet
Just like AWE (discussed in the previous chapter), the Pagelet Wizard is an application class framework that we can extend through metadata configuration and the development of custom application classes. Pagelets are composed of the following components: ■■ Data types A data type provides the source data for a pagelet. As you saw with the query pagelet we created in the previous section, data types have settings and parameters. For example, the URL data type has a URL setting and a Timeout parameter. At runtime, a data type uses these settings and parameters to retrieve the data that will be transformed and formatted according to the rest of the Pagelet Wizard’s configuration steps. ■■ Transformers A display format determines how to transform and display the data provided by a data type. For example, the PS Query data type offers a variety of options for formatting query results. The most flexible option is the Custom display format because it allows you to transform a data type’s results using XSL. ■■ Display formats The Pagelet Wizard uses transformers with display formats to transform a data type’s return value into a format suitable for display in a user’s browser. Transformers are independent of data types and display formats. Display formats, however, depend on transformers to generate results. When registering a data type, you must specify which display formats are suitable for that data type. Similarly, when you create a display format, you specify the transformer used by that display format.
Pagelet Data Types
PeopleTools delivers the following data types: ■■ HTML ■■ URL ■■ Query ■■ Search Record ■■ Integration Broker ■■ Navigation Collection The PeopleSoft Enterprise Portal includes additional data types for Enterprise Portal-specific modules. The Pagelet Wizard also allows you to create your own data types. As an example, we will create an e-mail data type that returns e-mail header information in XML format.
Setup for the Custom Data Type Example PeopleTools contains the MultiChannel Framework (MCF) application class framework for communicating with Post Office Protocol (POP) and Internet Message Access Protocol (IMAP) e-mail servers. These application classes depend on system objects that require some configuration
Chapter 4: Pagelet Wizard
149
prior to usage. For the following example to work, you will need to configure the delivered MCF_ GETMAIL Integration Broker node and service operation routings. In the MCF_GETMAIL node, switch to the Connectors tab and set the MCF_Port and MCF_Protocol values to settings that are appropriate for your e-mail server. We will specify the server, username, and password as part of the data type, so you don’t need to configure those connector values for this example. After configuring the node’s connector properties, ensure that the service operation routings and corresponding services and service operations are active. Since MCF uses Integration Broker to communicate with the e-mail server, be sure to configure your integration gateway and the application server’s publication and subscription handlers (referred to as pub/sub handlers in psadmin, the application server administration utility). Chapters 12 and 14 make extensive use of Integration Broker, so now is a good time to make sure you have integration working. The viewlet http://blogs.oracle.com/peopletools/gems/ibsetupws.exe provides step-by-step instructions for configuring the Integration Broker. PeopleBooks provides additional documentation describing MCF and Integration Broker configuration. Our objective is to create a data type that will fetch e-mail headers from a user’s e-mail account and wrap those headers in XML. In order to connect to an e-mail server, PeopleSoft needs to know the target node name, the e-mail server, the user’s e-mail account ID, and the user’s password. Our criteria for determining whether a pagelet’s variable is a setting or a parameter is the variability of the variable. If a variable varies by user, then it is a parameter. If it varies by pagelet, but is constant across users, then it is a setting. Since most companies have only one main e-mail server, and since we know the node name, we will make these two variables data type settings. The values for these settings may change from pagelet to pagelet, but not from user to user. We will create the username and password variables as data type parameters because they will vary from user to user. Remember the query pagelet we created earlier? The query name is a setting; therefore, we set its value at design time at step 2. In step 3, we had the option of allowing users to configure the query data type’s parameters or specifying values at design time.
Coding the Custom Data Type Let’s start coding this data type by creating a new application package named APT_PAGELET. Within this application class, add the subpackage DataSource. To the DataSource subpackage, add the application class EMailDataSource. After creating the application class structure, the fully qualified name for the EMailDataSource, will be APT_PAGELET:DataSou rce:EMailDataSource. Starting with imports and other definitions, we will step through the code required for this data type. You will find the full EMailDataSource code listing at the end of this section. I suggest you wait until reaching the full code listing before adding any code to the EMailDataSource. Note The Pagelet Wizard does not enforce package and class naming conventions. Therefore, when creating your own data types, you can structure your application package in a manner that suits your needs. import PTPPB_PAGELET:DataSource:DataSource; import PTPPB_PAGELET:UTILITY:Collection; import PTPPB_PAGELET:UTILITY:Setting;
150
PeopleSoft PeopleTools Tips & Techniques
/* All Pagelet Wizard Data Types must extend the abstract class * PTPPB_PAGELET:DataSource:DataSource */ class EMailDataSource extends PTPPB_PAGELET:DataSource:DataSource method EMailDataSource(&id_param As string); method initializeSettings(&NewSettings As PTPPB_PAGELET:UTILITY:Collection); method processSettingsChange(); method execute() Returns string; method Clone() Returns object; end-class;
The PTPPB_PAGELET:DataSource:DataSource contains four abstract methods. As you learned in Chapter 1, abstract methods are methods with a signature, but no implementation. These methods form a contract between the Pagelet Wizard and the developer. Since the DataSource base class provides default implementations for several of its methods, the only methods we need to declare are the constructor and the base class’s abstract methods.
The DataSource Constructor Let’s implement the application class constructor. method EMailDataSource /+ &id_param as String +/ %Super = create PTPPB_PAGELET:DataSource:DataSource(&id_param); %This.setObjectSubType("APT_EMAIL"); %This.setCanHaveParameters( True); %This.initializeSettings(%This.Settings); %This.hasSourceDataLinkHTML = False; end-method;
Since the base class’s constructor contains parameters, this class must also have a constructor that tells PeopleSoft how to create an instance of the base (%Super) class. The &id_param constructor parameter represents the metadata ID for this data type in the Pagelet Wizard metadata repository. After coding this data type, we will create an ID for this data type by registering it in the Pagelet Wizard’s metadata repository. We won’t use this value directly, but will pass it along to the base class constructor. The rest of the method calls in the constructor configure default values for this data type. For example, setting hasSourceDataLinkHTML to False tells the Pagelet Wizard that this data type does not provide a link to the pagelet’s underlying source data.
The initializeSettings Abstract Method Data type constructors call the initializeSettings abstract method. The initializeSettings method is responsible for configuring the data type setting fields. Setting configuration consists of identifying the field type, prompt, and display order. The following initializeSettings method configures the server name and node name setting fields. method initializeSettings /+ &NewSettings as PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.initializeSettings +/
Chapter 4: Pagelet Wizard
151
Local PTPPB_PAGELET:UTILITY:Setting &server; Local PTPPB_PAGELET:UTILITY:Setting &node; Local string &nodeLabel; If &NewSettings = Null Then &NewSettings = create PTPPB_PAGELET:UTILITY:Collection( "APT_EMailDataSourceSettings"); End-If; %This.setSettings(&NewSettings); &nodeLabel = CreateRecord(Record.PSMSGNODEDEFN) .MSGNODENAME.GetLongLabel("MSGNODENAME"); &server = %This.initDefaultSetting(&SERVER_SETTING_, "E-mail Server"); &node = %This.initDefaultSetting(&NODE_SETTING_, &nodeLabel); REM ** Set node prompt table; &node.EditType = &node.EDITTYPE_PROMPTTABLE; &node.PromptTable = "PSNODEDEFNVW"; If (%This.settingHasAValue(&SERVER_SETTING_) And %This.settingHasAValue(&NODE_SETTING_)) Then %This.setSettingsComplete( True); End-If; end-method;
Note The preceding code listing uses string literals for label text. PeopleSoft recommends storing strings in message catalog definitions. The message catalog offers multilingual support and online maintenance. The remainder of the code in this chapter uses string literals to simplify code examples. In practice, you should use message catalog definitions. Let’s break the initializeSettings method into manageable segments. The first segment contains the method signature and two setting declarations: &server and &node. method initializeSettings /+ &NewSettings as PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.initializeSettings +/ Local PTPPB_PAGELET:UTILITY:Setting &server; Local PTPPB_PAGELET:UTILITY:Setting &node; Local string &nodeLabel;
The method signature has a parameter named &NewSettings. When we implement the abstract Clone method, we will call the initializeSettings method, providing settings
152
PeopleSoft PeopleTools Tips & Techniques
from the original. When cloned, the initializeSettings method should use settings from the &NewSettings method parameter. The next code segment creates a new settings collection if necessary, and then attaches that collection to this object instance by calling the setSettings method. If &NewSettings = Null Then &NewSettings = create PTPPB_PAGELET:UTILITY:Collection( "APT_EMailDataSourceSettings"); End-If; %This.setSettings(&NewSettings);
Here’s the third segment: &nodeLabel = CreateRecord(Record.PSMSGNODEDEFN) .MSGNODENAME.GetLongLabel("MSGNODENAME"); &server = %This.initDefaultSetting(&SERVER_SETTING_, "E-mail Server"); &node = %This.initDefaultSetting(&NODE_SETTING_, &nodeLabel);
This segment stands out from the rest because it uses a method we haven’t created yet. As I was writing the initializeSettings method, I noticed each setting required similar initialization. Rather than write the same code for each setting, I chose to create a private helper method to create and initialize common setting values. This section of the initializeSettings method also uses two variables we haven’t declared: &SERVER_SETTING_ and &NODE_SETTING_. These variables represent constants we will declare in the application class declaration. Rather than risk mistyping a setting name in the various code locations that refer to the setting by name, I chose to create a compiler checked constant. Unlike an unchecked string, if you incorrectly spell the constant, the PeopleCode compiler will throw an error. You will see the constant declaration in the final code listing at the end of this section. The last item we will cover regarding this segment is the &nodeLabel variable. Whenever possible, we want to reuse existing configurations, object definitions, and so on. Rather than create a message catalog definition or hard-code a label for a well-used, well-known delivered field, I chose to reuse the existing node field label. This ensures that the label you use in the Pagelet Wizard is the same label a user would see when viewing nodes from the Integration Broker node configuration page. The fourth segment of the code contains the following lines: REM ** Set node prompt table; &node.EditType = &node.EDITTYPE_PROMPTTABLE; &node.PromptTable = "PSNODEDEFNVW";
The &server setting is a free-form text field with no validation. As such, the initDefaultSetting method performs the entire setting’s configuration. The &node setting, on the other hand, represents a predefined Integration Broker node. Therefore, we can use a prompt table to identify and validate the user-chosen value. The lines in this segment further configure the &node setting by specifying the edit type and prompt table. The final segment in this method calls the setSettingsComplete method, which is the method that controls the Pagelet Wizard step 2 Next button.
Chapter 4: Pagelet Wizard
153
If (%This.settingHasAValue(&SERVER_SETTING_) And %This.settingHasAValue(&NODE_SETTING_)) Then %This.setSettingsComplete( True); End-If; end-method;
Since the server name and node name values are required by the MCF e-mail application classes, we don’t want a user to move to step 3 until these fields contain valid values. This code executes a method named settingHasAValue to determine whether a setting has a value. This determination requires a couple of tests. Rather than code each test for each field in the initializeSettings method, and then again in the processSettingsChange method, I chose to centralize the logic in a single helper method. I believe this design decision helps the code read more like a sentence and less like obfuscated code. The following code listing describes the settingHasAValue method: method settingHasAValue /+ &settingName as String +/ /+ Returns Boolean +/ Local PTPPB_PAGELET:UTILITY:Setting &setting; REM ** Look up the setting in the settings collection; &setting = %This.Settings.getItemByID(&settingName); REM ** Is it null? If so, then return False; If &setting = Null Then Return False End-If; REM ** Is it a zero length string? If so, then return False; If &setting.Value = "" Then Return False; End-If; REM ** The setting passed all the tests, so it must have a value; Return True; end-method;
In the initializeSettings method, we test for the presence of a value (positive test). In the processSettingsChange method, we will test for the absence of a value (negative test). Rather than write If statements that use the Not operator, let’s write a helper method that is the inverse of the settingHasAValue method, appropriately named settingNeedsAValue. Multicriteria positive tests are much easier to read than multicriteria tests for negative values. method settingNeedsAValue /+ &settingName as String +/ /+ Returns Boolean +/ Return ( Not %This.settingHasAValue(&settingName)); end-method;
154
PeopleSoft PeopleTools Tips & Techniques
The settingNeedsAValue method is really just an alias for Not %This.settingHasAValue. It is my opinion that this alias is much easier to read than If ((Not ...) Or (Not ...) Then. While we’re on the subject of helper methods, here is the code for the initDefaultSetting method introduced a few paragraphs earlier: method initDefaultSetting /+ &settingId as String, +/ /+ &settingLabel as String +/ /+ Returns PTPPB_PAGELET:UTILITY:Setting +/ Local PTPPB_PAGELET:UTILITY:Setting &setting; &setting = %This.Settings.getItemByID(&settingId); If &setting = Null Then &setting = %This.createSettingProperty(&settingId, ""); End-If; &setting.EditType = &setting.EDITTYPE_NOTABLEEDIT; &setting.FieldType = &setting.FIELDTYPE_CHARACTER; &setting.Enabled = True; &setting.Visible = True; &setting.RefreshOnChange = True; &setting.Required = True; &setting.LongName = &settingLabel; &setting.setObjectToRefreshOnValueChange(%This); Return &setting; end-method;
When implementing the initializeSettings method, it is important to consider the case where a user creates, configures, saves, and closes a pagelet, only to reopen it later. When a user opens an existing pagelet definition, the Next button should be enabled, since all of the settings were previously set. Also, all the appropriate settings should be visible and enabled, just as they would be after finishing the Pagelet Wizard’s second step.
The processSettingsChange Abstract Method The next abstract method to implement is the processSettingsChange method. The Pagelet Wizard calls this method for each FieldChange event in step 2. This allows you to enable, disable, and redefine setting fields based on values chosen by the user. A good example of this is the Integration Broker data type, which is implemented in the PTPPB_ PAGELET:DataSource:IBDataSource application class. After the user selects the Integration Broker data type in step 2, the Pagelet Wizard displays only the Service Operation prompt field. After the user selects a service operation, the Pagelet Wizard displays the Receiver Node Name prompt field. The following code defines the EMailDataSource processSettingsChange method: method processSettingsChange /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.processSettingsChange +/
Chapter 4: Pagelet Wizard
155
/* do nothing if no setting or if a setting is missing */ rem Local PTPPB_PAGELET:UTILITY:Collection &outputs; Local PTPPB_PAGELET:DataSource:DataSourceParameter &parm; REM ** return if settings aren't valid; If (%This.settingNeedsAValue(&SERVER_SETTING_) Or %This.settingNeedsAValue(&NODE_SETTING_)) Then %This.setSettingsComplete( False); Return; End-If; /* We are finished with the Data Type settings. Show input and * output fields, if any, and then set up parameters for step 3. */ rem &outputs = create PTPPB_PAGELET:UTILITY:Collection( "OutputFields"); rem ... add output fields here...; rem %This.setOutputFields(&outputs); %This.initDefaultParameter(&USERNAME_PARM_, "User Name").Required = False; %This.initDefaultParameter(&PASSWORD_PARM_, "Password").Required = False; &parm = %This.initDefaultParameter(&MAXMSGCOUNT_PARM_, "Maximum Message Count"); &parm.DefaultValue = "10"; &parm.FieldType = &parm.FIELDTYPE_NUMBER; /* Add settings as internal parameters to save them with the * rest of the pagelet's data; */ &parm = %This.initDefaultParameter(&SERVER_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&SERVER_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL; &parm = %This.initDefaultParameter(&NODE_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&NODE_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL; /* and set the ParameterCollection to be immutable */ %This.getParameterCollection().setImmutable(); %This.setSettingsComplete( True); end-method;
A processSettingsChange method should start by validating the data type’s settings. In our case, we want to validate the server and node name. For simplicity, we test only for the presence of a value. We could further verify the server setting by actually trying to connect to the e-mail server. If any data type setting is invalid or incorrect, it is important to call %This .setSettingsComplete( False) and then return without further processing. Once all
156
PeopleSoft PeopleTools Tips & Techniques
settings have appropriate values, call %This.setSettingsComplete( True) to allow the user to move to step 3. The Pagelet Wizard will not enable the Next button until the DataSource.SettingsComplete property evaluates to True. Some data types have input and output parameters that change based on the data type’s settings. For example, the query pagelet we created earlier in this chapter had output fields that were determined by the query name chosen in step 2. The Pagelet Wizard shows the output fields to the user in step 2. If that query had prompts, those prompts would have appeared above the output fields as input fields. You set up the output fields collection after validating the data type’s settings. The processSettingsChange code contains a declaration for an output field collection, but I commented it out since it isn’t used by the EMailDataSource. At this point in the wizard, the user is ready to move to step 3. This step displays the list of data type parameters that a home page user may want to modify. Parameters differ from settings. Parameters are pagelet instance-specific, whereas settings are common to all instances of a pagelet. In our case, we will have parameters for the user’s e-mail username and password. Whereas the server name is common to all instances of the pagelet, the user’s credentials are specific to the user. The next section of the processSettingsChange method initializes the step 3 parameters: %This.initDefaultParameter(&USERNAME_PARM_,( "User Name").Required = False; %This.initDefaultParameter(&PASSWORD_PARM_, "Password").Required = False; &parm = %This.initDefaultParameter( &MAXMSGCOUNT_PARM_, "Maximum Message Count"); &parm.DefaultValue = "10"; &parm.FieldType = &parm.FIELDTYPE_NUMBER;
Just as with the setting initialization code in initializeSettings, I’ve taken the coding-byexception approach. After writing 30 lines of common code, I centralized that common code into the initDefaultParameter private method, and then used my processSettingsChange code to set only the parameter properties that are exceptions to the default properties.
Coding by Exception and Other Antipatterns Coding by exception is a pattern whereby programmers code only for exceptions. For example, the initDefaultSetting and initDefaultParameter methods set up defaults so that I code just for exceptional cases. The effect of applying this pattern to the initializeSettings and processSettingsChange methods is a reduction of 30 lines of code per method. However, it isn’t the reduction in lines that I appreciate; it is the clarity of a code listing that is 30 lines shorter. Removing 60 lines of redundant code is like tuning in a fuzzy radio station. Communication is clearer without all that static. Many programmers consider coding by exception to be an antipattern. By definition, an antipattern is a common but ineffective design pattern. Based on this definition, I’m sure every pattern becomes an antipattern when taken to extremes. Since my goal for using the pattern was to improve the clarity of my code, I consider the application of this pattern to be effective.
Chapter 4: Pagelet Wizard
157
Another complaint raised by well-known design pattern enthusiasts is that coding by exception degrades performance. If you look closely at the processSettingsChange method and the initDefaultParameter method, you will notice some redundancy. For example, the initDefaultParameter method sets a parameter’s required property to True. For some of the parameters, the processSettingsChange method immediately resets that property to False. This is a clear redundancy that may impact performance. The redundancy in this case, however, is no different from the redundancy involved in initializing many PeopleCode variables. Consider the following code: Local number &maxValue = 10; Local number &index; For &index = 1 to &maxValue ... End-For
At runtime, the variable &index has an initial value of 0. That value is set when the variable is declared and is immediately available for use. However, as you can see from the code listing, I do not intend to use the value 0. In fact, I don’t actually set the value of &index to a meaningful number until I use it in the For loop. In this case, the redundant initialization of the &index variable is a necessary part of the PeopleCode language. It is not possible to declare a number, string, or Boolean variable without initializing it, because the PeopleCode runtime implicitly initializes primitive variable types at declaration. Does this have an impact on performance? Of course, but I doubt the impact is material. Redundant initialization becomes important when dealing with constructed objects. Creating an in-memory instance of a record requires significantly more resources than setting the value of a numeric memory pointer. Instantiating a record, for example, may cause the PeopleCode runtime to execute SQL statements to collect the record’s metadata. Therefore, I would think twice about using coding by exception to initialize object variables like records, application classes, and so on. I believe it is this type of exception coding that led to the modern assessment of coding by exception. This is no surprise, considering how modern languages focus on objects. Patterns (design patterns or antipatterns) have a significant impact on the programs we create. Knowing them and using them effectively can have a dramatic positive impact. Unfortunately, the converse is also true. Using them ineffectively can have a dramatic negative impact.
As I previously mentioned, pagelets have two types of fields: settings and parameters. The Pagelet Wizard will save parameter values in the database, but not setting values. You are responsible for persisting setting values. Parameters can be stored in common field types, whereas data type settings may require special field specifications. For example, the HTML data type allows you to enter an unlimited amount of text in a setting field. With a setting like this, you may need to create a stand-alone record for storing that unlimited quantity of text. After a pagelet’s settings pass validation, you need to implement some form of persistence. You can find several examples of persistence methods in the processSettingsChange
158
PeopleSoft PeopleTools Tips & Techniques
method of the delivered PTPPB_PAGELET:DataSource classes. One method is to copy settings into internal parameters. This is the approach I used in the processSettingsChange code. The following lines are an excerpt from that method and show how I used exception-based coding to change the usage type from user-specified to internal. Internal parameters are stored with regular pagelet parameters, but are not visible to the user. /* Add settings as internal parameters to save them with the * rest of the pagelet's data; */ &parm = %This.initDefaultParameter(&SERVER_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&SERVER_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL; &parm = %This.initDefaultParameter(&NODE_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&NODE_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL;
The processSettingsChange method executes a private method named initDefaultParameter. The initDefaultParameter method is similar to the initDefaultSetting method described earlier, but with one key difference. Since the Pagelet Wizard stores parameter information, including the usage type, we don’t need to reinitialize these properties. The following listing contains the code for the initDefaultParameter method: method initDefaultParameter /+ &parmId as String, +/ /+ &parmLabel as String +/ /+ Returns PTPPB_PAGELET:DataSource:DataSourceParameter +/ Local PTPPB_PAGELET:DataSource:DataSourceParameter &parm; Local PTPPB_PAGELET:UTILITY:Collection &coll; &coll = %This.getParameterCollection(); &parm = &coll.getItemByID(&parmId); REM ** Create parameter and set default values; If (&parm = Null) Then &parm = create PTPPB_PAGELET:DataSource:DataSourceParameter( &parmId); &parm.LongName = &parmLabel; &parm.FieldType = &parm.FIELDTYPE_CHARACTER; &parm.UsageType = &parm.USAGETYPE_USERSPECIFIED; &parm.Required = True; &coll.Insert(&parm); End-If; Return &parm; end-method;
Chapter 4: Pagelet Wizard
159
The Clone Abstract Method The Clone method is supposed to create an exact copy of the current object. Cloning involves more than just copying values from one object to the next. If an object contains objects, then the nested object must be cloned as well. The following code listing demonstrates a Clone method implementation: method Clone /+ Returns Object +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.Clone +/ Local APT_PAGELET:DataSource:EMailDataSource © = create APT_PAGELET:DataSource:EMailDataSource(%This.ID); ©.PageletID = %This.PageletID; ©.ParameterCollection = %This.ParameterCollection.Clone(); REM ** If you have output fields, then uncomment next line; rem ©.OutputFields = %This.OutputFields.Clone(); ©.initializeSettings(%This.Settings.Clone()); Return © end-method;
Notice that the Clone method deep clones object properties like the Settings property. A shallow clone would cause the main object’s properties and the new cloned object’s properties to point to the same in-memory objects. I commented out the deep clone for OutputFields. If you create a data type that uses output fields, be sure to clone the OutputFields as well.
The execute Abstract Method The execute method is responsible for collecting data from its source and returning it in string form so the Pagelet Wizard can format the data for display. In our example data type, this means the data type must read parameters from the pagelet’s parameters collection, connect to the e-mail server using the MCF application classes, and then format the returned e-mail headers as XML. Note Pagelet Wizard data types generally return XML because XML can be formatted in a variety of ways using XSL. For example, using XSL, you can convert XML into SVG or Flash charts, RTF, or standard HTML. XSL is a very powerful transformation language. If you aren’t already familiar with XSL, I encourage you to learn it. method execute /+ Returns String +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.execute +/ Local PTPPB_PAGELET:UTILITY:Collection ¶mColl; Local PT_MCF_MAIL:MCFGetMail &getMail; Local PT_MCF_MAIL:MCFInboundEmail &email; Local array of PT_MCF_MAIL:MCFInboundEmail &emailArr; Local XmlDoc &xmlDoc; Local XmlNode &xmlDocNode; Local XmlNode &rowNode;
160
PeopleSoft PeopleTools Tips & Techniques Local Local Local Local Local Local Local Local
XmlNode &dataNode; string &userNameValue; string &passwordValue; string &server; string &nodeName; string &dttmSent; number &maxMsgCount; number &msgIndex = 0;
¶mColl = %This.getParameterCollection(); REM ** Read parameters from saved collection; &maxMsgCount = Value(¶mColl.getItemByID( &MAXMSGCOUNT_PARM_).evaluatedValue()); &server = ¶mColl.getItemByID( &SERVER_SETTING_).evaluatedValue(); &nodeName = ¶mColl.getItemByID( &NODE_SETTING_).evaluatedValue(); &userNameValue = ¶mColl.getItemByID( &USERNAME_PARM_).evaluatedValue(); &passwordValue = ¶mColl.getItemByID( &PASSWORD_PARM_).evaluatedValue(); REM ** Fetch e-mails; &getMail = create PT_MCF_MAIL:MCFGetMail(); &getMail.SetMCFEmail(&userNameValue, &passwordValue, &server, &nodeName); &emailArr = &getMail.ReadEmails(&maxMsgCount); REM ** Create an XML Document containing e-mails; &xmlDoc = CreateXmlDoc( ""); &xmlDocNode = &xmlDoc.DocumentElement; If (&getMail.Status 0) Then &rowNode = &xmlDocNode.AddElement("error"); &rowNode.AddAttribute("status", String(&getMail.Status)); Else While &emailArr.Next(&msgIndex) &email = &emailArr [&msgIndex]; REM ** Human readable date format; &dttmSent = DateTimeToLocalizedString( &email.DttmSent, "EEE, MMM d, yyyy 'at' HH:mm:ss"); &rowNode = &xmlDocNode.AddElement("message"); &rowNode.AddAttribute("id", &email.UID); &dataNode = &rowNode.AddElement( "from").AddText(&email.From);
Chapter 4: Pagelet Wizard
161
&dataNode = &rowNode.AddElement( "to").AddText(&email.NotifyTo); &dataNode = &rowNode.AddElement( "cc").AddText(&email.NotifyCC); &dataNode = &rowNode.AddElement( "subject").AddText(&email.Subject); &dataNode = &rowNode.AddElement( "date-sent").AddText(&dttmSent); End-While; End-If; Return &xmlDoc.GenXmlString(); end-method;
Following the variable declarations in the preceding code are five lines of code that read parameter values into local variables. Notice that I use the evaluatedValue property of each parameter. As you saw in step 3 of the Pagelet Wizard, parameter values can be derived from system variables. The evaluatedValue property takes this into account and returns the correct value for the parameter, regardless of the parameter’s UsageType. The next three lines use the MCFGetMail application class to read messages from the e-mail server. The MCF classes abstract the details of working with the Integration Broker GETMAILTARGET target connector. For more information about the MCFGetMail application class, see the PeopleCode API Reference. The rest of the code in this method uses the PeopleCode XML classes to generate an XML document from an array of e-mail messages.
Templates The execute method used by the Pagelet Wizard generates the same document structure every time it runs. The detail values and row count may differ between executions, but the structure is always the same. With careful study, it is possible to infer the structure of the generated XML document, but the actual structure is not readily discernible. Unfortunately, the easiest way to determine the shape of the XML document is to run the execute method and view its results. Templates, on the other hand, look like the result document, but with variables. An HTML definition is an example of a template. HTML definitions contain all the markup required to create a result document and may have bind variables to insert dynamic, runtime-evaluated information. Since the Pagelet Wizard always executes online, we could mock up the resultant XML document in an HTML definition. HTML definitions, however, don’t support control flow statements (loops, conditional logic, and so on). One workaround is to create two HTML definitions: one main document template and one row template. Another alternative is to use a Java template engine. In Chapter 9, you will learn how to use the Apache Velocity template engine to generate structured documents from PeopleCode objects.
162
PeopleSoft PeopleTools Tips & Techniques
The EMailDataSource Code Listing The complete code listing for our APT_PAGELET:DataSource:EMailDataSource follows. This code listing contains a few details that were missing from the previous listings. For example, the previous listings used constants that were never declared. The following listing contains those declarations. import import import import
PTPPB_PAGELET:DataSource:DataSourceParameter; PTPPB_PAGELET:DataSource:DataSource; PTPPB_PAGELET:UTILITY:Collection; PTPPB_PAGELET:UTILITY:Setting;
import PT_MCF_MAIL:MCFGetMail; import PT_MCF_MAIL:MCFInboundEmail; /* All Pagelet Wizard Data Types must extend the abstract class * PTPPB_PAGELET:DataSource:DataSource */ class EMailDataSource extends PTPPB_PAGELET:DataSource:DataSource method EMailDataSource(&id_param As string); method initializeSettings( &NewSettings As PTPPB_PAGELET:UTILITY:Collection); method processSettingsChange(); method execute() Returns string; method Clone() Returns object; private method initDefaultSetting(&settingId As string, &settingLabel As string) Returns PTPPB_PAGELET:UTILITY:Setting; method initDefaultParameter(&parmId As string, &parmLabel As string) Returns PTPPB_PAGELET:DataSource:DataSourceParameter; method settingHasAValue(&settingName As string) Returns boolean; method settingNeedsAValue(&settingName As string) Returns boolean; REM ** Step 2 setting names; Constant &SERVER_SETTING_ = "server"; Constant &NODE_SETTING_ = "node"; REM ** Step 3 parameters; Constant &USERNAME_PARM_ = "username"; Constant &PASSWORD_PARM_ = "password"; Constant &MAXMSGCOUNT_PARM_ = ".MAXMSGCOUNT"; end-class; /* Constructor */ method EMailDataSource /+ &id_param as String +/ %Super = create PTPPB_PAGELET:DataSource:DataSource(&id_param); %This.setObjectSubType("APT_EMAIL");
Chapter 4: Pagelet Wizard %This.setCanHaveParameters( True); %This.initializeSettings(%This.Settings); %This.hasSourceDataLinkHTML = False; end-method; method initializeSettings /+ &NewSettings as PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.initializeSettings +/ Local PTPPB_PAGELET:UTILITY:Setting &server; Local PTPPB_PAGELET:UTILITY:Setting &node; Local string &nodeLabel; If &NewSettings = Null Then &NewSettings = create PTPPB_PAGELET:UTILITY:Collection( "APT_EMailDataSourceSettings"); End-If; %This.setSettings(&NewSettings); &nodeLabel = CreateRecord(Record.PSMSGNODEDEFN) .MSGNODENAME.GetLongLabel("MSGNODENAME"); &server = %This.initDefaultSetting(&SERVER_SETTING_, "E-mail Server"); &node = %This.initDefaultSetting(&NODE_SETTING_, &nodeLabel); REM ** Set node prompt table; &node.EditType = &node.EDITTYPE_PROMPTTABLE; &node.PromptTable = "PSNODEDEFNVW"; If (%This.settingHasAValue(&SERVER_SETTING_) And %This.settingHasAValue(&NODE_SETTING_)) Then %This.setSettingsComplete( True); End-If; end-method; method processSettingsChange /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.processSettingsChange +/ /* do nothing if no setting - or if a setting is missing */ rem Local PTPPB_PAGELET:UTILITY:Collection &outputs; Local PTPPB_PAGELET:DataSource:DataSourceParameter &parm; REM ** return if settings aren't valid; If (%This.settingNeedsAValue(&SERVER_SETTING_) Or %This.settingNeedsAValue(&NODE_SETTING_)) Then %This.setSettingsComplete( False); Return; End-If;
163
164
PeopleSoft PeopleTools Tips & Techniques /* We are finished with the Data Type settings, show input and * output fields, if any, and then set up parameters for step 3. */ rem &outputs = create PTPPB_PAGELET:UTILITY:Collection("OutputFields"); rem ... add output fields here...; rem %This.setOutputFields(&outputs); %This.initDefaultParameter(&USERNAME_PARM_, "User Name").Required = False; %This.initDefaultParameter(&PASSWORD_PARM_, "Password").Required = False; &parm = %This.initDefaultParameter(&MAXMSGCOUNT_PARM_, "Maximum Message Count"); &parm.DefaultValue = "10"; &parm.FieldType = &parm.FIELDTYPE_NUMBER; /* Add settings as internal parameters to save them with the * rest of the pagelet's data; */ &parm = %This.initDefaultParameter(&SERVER_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&SERVER_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL; &parm = %This.initDefaultParameter(&NODE_SETTING_, ""); &parm.Value = %This.Settings.getItemByID(&NODE_SETTING_).Value; &parm.UsageType = &parm.USAGETYPE_INTERNAL; /* and set the ParameterCollection to be immutable */ %This.getParameterCollection().setImmutable();
%This.setSettingsComplete( True); end-method; method execute /+ Returns String +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.execute +/ Local PTPPB_PAGELET:UTILITY:Collection ¶mColl; Local PT_MCF_MAIL:MCFGetMail &getMail; Local PT_MCF_MAIL:MCFInboundEmail &email; Local array of PT_MCF_MAIL:MCFInboundEmail &emailArr; Local XmlDoc &xmlDoc; Local XmlNode &xmlDocNode; Local XmlNode &rowNode; Local XmlNode &dataNode; Local string &userNameValue; Local string &passwordValue; Local string &server; Local string &nodeName;
Chapter 4: Pagelet Wizard Local string &dttmSent; Local number &maxMsgCount; Local number &msgIndex = 0; ¶mColl = %This.getParameterCollection(); REM ** Read parameters from saved collection; &maxMsgCount = Value(¶mColl.getItemByID( &MAXMSGCOUNT_PARM_).evaluatedValue()); &server = ¶mColl.getItemByID( &SERVER_SETTING_).evaluatedValue(); &nodeName = ¶mColl.getItemByID( &NODE_SETTING_).evaluatedValue(); &userNameValue = ¶mColl.getItemByID( &USERNAME_PARM_).evaluatedValue(); &passwordValue = ¶mColl.getItemByID( &PASSWORD_PARM_).evaluatedValue(); REM ** Fetch e-mails; &getMail = create PT_MCF_MAIL:MCFGetMail(); &getMail.SetMCFEmail(&userNameValue, &passwordValue, &server, &nodeName); &emailArr = &getMail.ReadEmails(&maxMsgCount); REM ** Create an XML Document containing e-mails; &xmlDoc = CreateXmlDoc( ""); &xmlDocNode = &xmlDoc.DocumentElement; If (&getMail.Status 0) Then &rowNode = &xmlDocNode.AddElement("error"); &rowNode.AddAttribute("status", String(&getMail.Status)); Else While &emailArr.Next(&msgIndex) &email = &emailArr [&msgIndex]; REM ** Human readable date format; &dttmSent = DateTimeToLocalizedString(&email.DttmSent, "EEE, MMM d, yyyy 'at' HH:mm:ss"); &rowNode = &xmlDocNode.AddElement("message"); &rowNode.AddAttribute("id", &email.UID); &dataNode = &rowNode.AddElement( "from").AddText(&email.From); &dataNode = &rowNode.AddElement( "to").AddText(&email.NotifyTo); &dataNode = &rowNode.AddElement( "cc").AddText(&email.NotifyCC); &dataNode = &rowNode.AddElement( "subject").AddText(&email.Subject);
165
166
PeopleSoft PeopleTools Tips & Techniques &dataNode = &rowNode.AddElement( "date-sent").AddText(&dttmSent); End-While; End-If;
Return &xmlDoc.GenXmlString(); end-method; /* Returns an exact duplicate of this EMailDataSource */ method Clone /+ Returns Object +/ /+ Extends/implements PTPPB_PAGELET:DataSource:DataSource.Clone +/ Local APT_PAGELET:DataSource:EMailDataSource © = create APT_PAGELET:DataSource:EMailDataSource(%This.ID); ©.PageletID = %This.PageletID; ©.ParameterCollection = %This.ParameterCollection.Clone(); REM ** If you have output fields, then uncomment next line; rem ©.OutputFields = %This.OutputFields.Clone(); ©.initializeSettings(%This.Settings.Clone()); Return © end-method; method initDefaultSetting /+ &settingId as String, +/ /+ &settingLabel as String +/ /+ Returns PTPPB_PAGELET:UTILITY:Setting +/ Local PTPPB_PAGELET:UTILITY:Setting &setting; Local PTPPB_PAGELET:DataSource:DataSourceParameter &parm; &setting = %This.Settings.getItemByID(&settingId); REM ** Create setting; If &setting = Null Then &setting = %This.createSettingProperty(&settingId, ""); End-If; REM ** Set default values; &setting.EditType = &setting.EDITTYPE_NOTABLEEDIT; &setting.FieldType = &setting.FIELDTYPE_CHARACTER; &setting.Enabled = True; &setting.Visible = True; &setting.RefreshOnChange = True; &setting.Required = True; &setting.LongName = &settingLabel; &setting.setObjectToRefreshOnValueChange(%This); Return &setting; end-method;
Chapter 4: Pagelet Wizard method initDefaultParameter /+ &parmId as String, +/ /+ &parmLabel as String +/ /+ Returns PTPPB_PAGELET:DataSource:DataSourceParameter +/ Local PTPPB_PAGELET:DataSource:DataSourceParameter &parm; Local PTPPB_PAGELET:UTILITY:Collection &coll; &coll = %This.getParameterCollection(); &parm = &coll.getItemByID(&parmId); REM ** Create parameter and set default values; If (&parm = Null) Then &parm = create PTPPB_PAGELET:DataSource:DataSourceParameter( &parmId); &parm.LongName = &parmLabel; &parm.FieldType = &parm.FIELDTYPE_CHARACTER; &parm.UsageType = &parm.USAGETYPE_USERSPECIFIED; &parm.Required = True; &coll.Insert(&parm); End-If; Return &parm; end-method; method settingHasAValue /+ &settingName as String +/ /+ Returns Boolean +/ Local PTPPB_PAGELET:UTILITY:Setting &setting; REM ** Look up the setting in the settings collection; &setting = %This.Settings.getItemByID(&settingName); REM ** Is it null? If so, then return False; If &setting = Null Then Return False End-If; REM ** Is it a zero length string? If so, then return False; If &setting.Value = "" Then Return False; End-If; REM ** The setting passed all the tests, so it must have a value; Return True; end-method; method settingNeedsAValue /+ &settingName as String +/
167
168
PeopleSoft PeopleTools Tips & Techniques
/+ Returns Boolean +/ Return ( Not %This.settingHasAValue(&settingName)); end-method;
Other DataSource Properties and Methods The DataSource abstract class contains a few other methods you can override to implement additional behavior. For example, you can override the getSourceDataLinkHTML method to provide a link to the pagelet’s source data. You can see a demonstration of this in the PTPPB_ PAGELET:DataSource:QueryDataSource application class. %This.personalizationInstructions and %This.configurizationInstructions are examples of properties you can set to customize the behavior of the Pagelet Wizard. If you look back at the EMailDataSource constructor, you will see the code: %This .setObjectSubType("APT_EMAIL"). The Pagelet Wizard uses this value to display the object type’s DTD. A DTD describes the structure of an XML document, including the document’s elements and attributes. The DTD is stored as an HTML definition with the name: PTPPB_" | %This.ObjectSubType | "DTD"
You are not required to create a DTD for custom data types. If you choose to create a DTD, you can view that DTD online from PeopleTools | Portal | Pagelet Wizard | Define Data Types. Providing a DTD helps other developers create display formats, transformers, and XSL templates for your data type.
Registering the Data Type The Pagelet Wizard needs some information about this data type before we can use it to create pagelets. To configure the data type’s metadata, navigate to PeopleTools | Portal | Pagelet Wizard | Define Data Types and add a new value named APT_EMAIL. When defining a data type, we must specify the data type’s application class, as well as the display formats that are compatible with this data type, as shown in Figure 4-8.
Creating a Test Pagelet When you click the Personalize Content or Personalize Layout links on your PeopleSoft home page, you will see a list of available pagelets. To help organize the Personalize Content page, pagelets are listed under headings. Before we create a test pagelet, let’s create a new heading.
Adding a New Pagelet Heading Headings are actually subfolders of the Portal Objects | Pagelets portal registry folder. To create a new subfolder, open the portal registry by navigating to PeopleTools | Portal | Structure and Content. Within the portal registry, open the folder Portal Objects and then the folder Pagelets. While viewing the contents of the Pagelets portal registry folder, click the Add Folder hyperlink. Figure 4-9 is a partial screen shot of the folder we will use for custom pagelets. Create and save this folder. We will refer to it when we create an e-mail pagelet.
Chapter 4: Pagelet Wizard
169
FIGURE 4-8. Pagelet Wizard data type metadata
Creating the E-Mail Pagelet Navigate to PeopleTools | Portal | Pagelet Wizard | Pagelet Wizard and create a new pagelet using the EMailDataSource data type (see Figures 4-10 through 4-15), as follows: 1. On the Specify Pagelet Information page (step 1), add the value APT_TEST_EMAIL. 2. On the Select Data Source page (step 2), select the E-mail data type. After selecting the data type, the server and message node fields should appear. These fields appear in response to the initializeSettings method that is called by the EMailDataSource constructor. The Pagelet Wizard constructs a new instance of the EMailDataSource from the data type FieldChange event. Figure 4-11 shows my EMailDataSource with my server. After you enter values for the server and node, the Pagelet Wizard enables the Next button in the upper-right corner. The Pagelet Wizard enables this button in response to the %This .setSettingsComplete( True) call in our processSettingsChange method.
170
PeopleSoft PeopleTools Tips & Techniques
FIGURE 4-9. APT_DEMO pagelet portal registry folder
Note Unless you are familiar with Integration Broker and MCF, I suggest you use the MCF_GETMAIL node, as shown Figure 4-11. 3. On the Specify Data Source page (step 3), you see the data type parameters we configured in the processSettingsChange method. Since we provided a default value for the only required parameter, the Pagelet Wizard automatically enables the Next button. As you recall from the query pagelet we created earlier, step 5 will generate a preview of the pagelet we are designing. Therefore, for design purposes, we will specify a username and password for the default values here. We will delete them before saving the pagelet, because we don’t want to pass these default values along to the user, since they will contain our e-mail server credentials. Figure 4-12 shows step 3 with the parameters populated.
Chapter 4: Pagelet Wizard
FIGURE 4-10. Pagelet Wizard step 1 for the e-mail pagelet
FIGURE 4-11. Pagelet Wizard step 2 for the e-mail pagelet
171
172
PeopleSoft PeopleTools Tips & Techniques
FIGURE 4-12. Pagelet Wizard step 3 for the e-mail pagelet
Note I’m sure you noticed that PeopleSoft does not mask your e-mail password as you type. The Pagelet Wizard does not have a password field type. This is important to note, as some organizations may not allow passwords to be displayed on the screen in plain text. Additionally, this example stores passwords in the database without encrypting them. We chose to persist parameter values using the standard Pagelet Wizard persistence mechanism, which doesn’t allow for encryption. With a little rework and PeopleSoft’s pluggable encryption1, you can easily remedy this situation. 4. On the Select Display Format page (step 4), we have only one option, the Custom display format. Therefore, just click Next to move on to step 5.
Chapter 4: Pagelet Wizard
173
FIGURE 4-13. Pagelet Wizard step 5 with the default preview for the e-mail pagelet
5. On the Specify Display Options page (step 5), you will be greeted by a very anticlimatic pagelet preview that says, “Enter the XSL or generate the XSL from an existing template,” as shown in Figure 4-13. The Custom display format allows us to transform an XML data type into something meaningful using XSL. The next item to note in step 5 is the XML text box, which contains the XML generated by our data type. Tip Take a look at the XML in the XML text box. Do you see a list of messages or an error message? The error handling in the EMailDataSource execute method returns the same message, regardless of the error returned by MCFGetMail. Before continuing, make sure you specified the correct server name, username, and password in steps 2 and 3. If those are correct, then verify that your application server can connect to your e-mail server.
174
PeopleSoft PeopleTools Tips & Techniques
FIGURE 4-14. Pagelet Wizard step 5 with custom XSL for the e-mail pagelet
So we can generate something meaningful from this data type, replace the existing XSL with the following code:
Chapter 4: Pagelet Wizard
FIGURE 4-15. Pagelet Wizard step 6
from
175
176
PeopleSoft PeopleTools Tips & Techniques Unable to connect to the e-mail server. Please check your user name and password settings. Error code:
After updating the XSL field with this XSL, you should see a pagelet preview that resembles Figure 4-14. Of course, if you want to see a list of e-mail messages, your inbox must contain messages. 6. When you are satisfied with your pagelet, move on to step 6 and choose to publish this pagelet as a Homepage Pagelet, in the new Demo Pagelets folder, as shown in Figure 4-15. 7. Before saving your pagelet, be sure to go back to step 3 and delete your e-mail username and password. We set these default values just so we could generate a pagelet preview in step 6. If you leave your username and password in step 3, these default values will be provided to every PeopleSoft user in your organization! We must make only one more change before we can add this pagelet to a home page.
Adding the Pagelet to the Home Page Tab List PeopleSoft applications can have multiple home page tabs. PeopleSoft allows an administrator to determine which pagelets should be available to each tab. Before users can add this new pagelet to a home page, we need to add the pagelet to a home page tab’s list of available content. To make this pagelet (and any other pagelet in the Demo Pagelets folder) available on the home page, follow these steps: 1. Open the portal registry by navigating to PeopleTools | Portal | Structure and Content. 2. In the portal registry, navigate to Portal Objects | Homepage | Tabs. 3. In the Tabs folder, click the edit link next to the content reference labeled My Page. 4. From the Content Ref Administration page, switch to the Tab Content tab. 6. Select the Include All check box in the Demo Pagelets group box, as shown in Figure 4-16. You now have a working pagelet created from a custom data type. Feel free to add it to your home page.
Chapter 4: Pagelet Wizard
177
FIGURE 4-16. My Page content reference tab content
Pagelet Transformers
As previously discussed, the Pagelet Wizard uses data types, display formats, and transformers. When we create pagelets, the Pagelet Wizard asks us to select a data type and a display format, but it doesn’t ask us about a transformer. A transformer is actually an assistant to a display format. After generating a display template for a given data source, a display format applies that display template to the data source using a transformer. PeopleSoft applications come with two configured transformers: ■■ The PASSTHRU transformer doesn’t modify the data in any way. It just passes the data type’s result along to the display format without any modifications. ■■ The XSL transformer uses XSL to transform the data type’s XML.
178
PeopleSoft PeopleTools Tips & Techniques
Just like a data source, a transformer has stored parameters. The XSL transformer, for example, expects one parameter named XSL. A display format, such as the Chart display format, generates XSL based on configuration options chosen in step 5 of the Pagelet Wizard, and then updates the transformer’s parameter collection. Generally speaking, transformers should be independent of the data type and display format. Neither the XSL or PASSTHRU transformer has knowledge of its data types and display formats. Transformers extend the abstract class PTPPB_PAGELET:Transformer:Transformer and must implement the abstract execute method as well as the Clone method. The PTPPB_ PAGELET:Transformer:PassthroughTransformer is the simplest example of a transformer because it just returns the data type’s data without transformation. If your transformer has parameters, you must implement the additional getParameterCollection method. The PTPPB_PAGELET:Transformer:XSLTransformer class is an example of a transformer with parameters. In Chapter 9, we will use Java and regular expressions to create a custom meta-HTML resolver. The following pseudo code is a prototype for a meta-HTML transformer based on that Chapter 9 meta-HTML resolver: import import import import
PTPPB_PAGELET:EXCEPTION:InvalidValueException; PTPPB_PAGELET:Transformer:Transformer; PTPPB_PAGELET:UTILITY:Collection; PTPPB_PAGELET:UTILITY:Parameter;
/* All Transformers extend PTPPB_PAGELET:Transformer:Transformer */ class MetaHTMLTransformer extends PTPPB_PAGELET:Transformer:Transformer method MetaHTMLTransformer(&ID_param As string); method getParameterCollection() Returns PTPPB_PAGELET:UTILITY:Collection; method execute(&pageletID As string) Returns string; method Clone() Returns PTPPB_PAGELET:Transformer:Transformer; end-class; /* Required constructor */ method MetaHTMLTransformer /+ &ID_param as String +/ %Super = create PTPPB_PAGELET:Transformer:Transformer(&ID_param); end-method; /* If your transformer has parameters, then implement the * getParameterCollection method. An example parameter is the XSL * parameter required by the XSLTransformer. */ method getParameterCollection /+ Returns PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:Transformer:Transformer.getParameterCollection +/ If %This.ParameterCollection Null Then Return %This.ParameterCollection; End-If;
Chapter 4: Pagelet Wizard /* Initialize collection since it doesn't exist (lazy * initialization) */ Local PTPPB_PAGELET:UTILITY:Parameter &parm; REM ** addParameter will create an empty collection; &parm = %This.addParameter("PARM1", ""); &parm = %This.addParameter("PARM2", ""); %This.ParameterCollection.setImmutable(); Return %This.ParameterCollection; end-method; /* The execute method is declared abstract in the base class. This * means you are required to implement the execute method. * * The execute method returns the transformed result */ method execute /+ &pageletID as String +/ /+ Returns String +/ /+ Extends/implements PTPPB_PAGELET:Transformer:Transformer.execute +/ REM ** Source data comes from %This.DataToTransform.Value; Local string &sourceText = %This.DataToTransform.Value; Local string &transformedText; REM ** Validate input; If (None(&sourceText)) Then throw create PTPPB_PAGELET:EXCEPTION:InvalidValueException( "Source Text", ""); End-If; REM ** Validate parameters, if any; Local string &parm1 = %This.ParameterCollection.getItemByID( "PARM1").Value; If (None(&parm1)) Then throw create PTPPB_PAGELET:EXCEPTION:InvalidValueException( "PARM1", ""); End-If; Local string &parm2 = %This.ParameterCollection.getItemByID( "PARM2").Value; If (None(&parm2)) Then throw create PTPPB_PAGELET:EXCEPTION:InvalidValueException( "PARM2", ""); End-If;
179
180
PeopleSoft PeopleTools Tips & Techniques REM ** TODO: transform sourceText into &transformedText;
Return &transformedText; end-method; /* Make a copy of this object. */ method Clone /+ Returns PTPPB_PAGELET:Transformer:Transformer +/ /+ Extends/implements PTPPB_PAGELET:Transformer:Transformer.Clone +/ Local APT_PAGELET:Transformer:MetaHTMLTransformer &t; &t = create APT_PAGELET:Transformer:MetaHTMLTransformer(%This.ID); If (%This.DataToTransform Null) Then &t.DataToTransform = %This.DataToTransform.Clone(); End-If; If (%This.ParameterCollection Null) Then &t.ParameterCollection = %This.ParameterCollection.Clone(); End-If; Return &t; end-method;
Just like data types, transformers need to be registered with the Pagelet Wizard metadata repository.
XSL Templates
As you may have noticed, XML and XSL are central to the Pagelet Wizard. Many of the Pagelet Wizard’s components expect data types to return data in XML format. For example, the PS Query data type returns query results as an XML document. A URL data type can return an RSS feed’s XML document. When we registered the EMailDataSource, we chose the Custom display format. This allowed us to write a custom XSL template to transform the e-mail message headers into HTML. Since we may use this same XML for another pagelet that connects to a different e-mail server, we may want to save it for reuse. You can register an XSL template for use with a particular data type by adding a new value to PeopleTools | Portal | Pagelet Wizard | Define XSL.
Display Formats
Display formats convert a data type into a formatted pagelet. As you saw in the “Data Types” section, you must tell the Pagelet Wizard which display formats are acceptable for a data type. PeopleTools delivers seven display formats. Most of the display formats dynamically generate XSL and then use the XSL transformer to create HTML. The simplest display format is the PASSTHRU format, which complements the PASSTHRU transformer. Its purpose is to pass the results of the data type through the Pagelet Wizard without modification. This is the preferred display format for the HTML and URL data types when those types return plain HTML. Since it is possible for either of those two data types to return XML, the PASSTHRU display format is not always appropriate, but it usually provides the best fit.
Chapter 4: Pagelet Wizard
181
Display formats follow a pattern similar to data types, but with the major difference that they require custom pages. These custom pages contain several delivered subpages plus any setting fields required for the custom display format. If you navigate to PeopleTools | Portal | Pagelet Wizard | Define Display Formats, you can see the names of the pages used for each of the display formats. If you create your own display format, I recommend cloning an existing display format page and modifying your copy accordingly. Display formats are application classes that extend the abstract class PTPPB_PAGELET:Tran sformBuilder:TransformBuilder. The code for a custom display format might look something like this: import import import import import import
PTPPB_PAGELET:TransformBuilder:TransformBuilder; PTPPB_PAGELET:Transformer:Transformer; PTPPB_PAGELET:UTILITY:Collection; PTPPB_PAGELET:UTILITY:Parameter; PTPPB_PAGELET:UTILITY:Setting; PTPPB_PAGELET:XSL:XSLDoc;
class MyCustomBuilder extends PTPPB_PAGELET:TransformBuilder:TransformBuilder method MyCustomBuilder(&id_param As string); method genTransformParameterCollection() Returns PTPPB_PAGELET:UTILITY:Collection; method initializeSettings( &NewSettings As PTPPB_PAGELET:UTILITY:Collection); method updateSettings(); method Clone() Returns PTPPB_PAGELET:TransformBuilder:TransformBuilder; private method generateXSL() Returns PTPPB_PAGELET:XSL:XSLDoc; end-class; method MyCustomBuilder /+ &id_param as String +/ %Super = create PTPPB_PAGELET:TransformBuilder:TransformBuilder(&id_param); %This.initializeSettings(%This.Settings); %This.Transformer = %This.getAssociatedTransformer(); end-method; method initializeSettings /+ &NewSettings as PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:TransformBuilder: TransformBuilder.initializeSettings +/ Local PTPPB_PAGELET:UTILITY:Setting &setting;
182
PeopleSoft PeopleTools Tips & Techniques %This.setSettings(&NewSettings); &setting = %This.Settings.getItemByID("MYSETTING1"); If &setting = Null Then &setting = %This.createSettingProperty("MYSETTING1", "default value"); End-If;
&setting = %This.Settings.getItemByID("LONG_TEXT"); If &setting = Null Then &setting = %This.createSettingProperty("LONG_TEXT", ""); End-If; &setting.FieldType = &setting.FIELDTYPE_LONGCHARACTER; end-method; /* Update internal settings on save */ method updateSettings /+ Extends/implements PTPPB_PAGELET:TransformBuilder: TransformBuilder.updateSettings +/ REM ** code to update settings; end-method; /* This Display Format assumes the transformer is the XSLTransformer, * or, at least a transformer with an XSL parameter. */ method genTransformParameterCollection /+ Returns PTPPB_PAGELET:UTILITY:Collection +/ /+ Extends/implements PTPPB_PAGELET:TransformBuilder: TransformBuilder.genTransformParameterCollection +/ Local string &xsl; Local PTPPB_PAGELET:Transformer:Transformer &trx; Local PTPPB_PAGELET:UTILITY:Parameter &parm; &trx = %This.Transformer; &parm = &trx.getParameterCollection().getItemByID("XSL"); &xsl = %This.generateXSL().GenXmlString(); If &parm = Null Then &parm = &trx.addParameter("XSL", &xsl); Else &parm.Value = &xsl; End-If; Return &trx.getParameterCollection(); end-method; method generateXSL /+ Returns PTPPB_PAGELET:XSL:XSLDoc +/
Chapter 4: Pagelet Wizard
183
Local PTPPB_PAGELET:XSL:XSLDoc &xslDoc; &xslDoc = create PTPPB_PAGELET:XSL:XSLDoc(); REM ** TODO: Add XSL element nodes to &xslDoc; Return &xslDoc; end-method; /* Copy this object */ method Clone /+ Returns PTPPB_PAGELET:TransformBuilder:TransformBuilder +/ /+ Extends/implements PTPPB_PAGELET:TransformBuilder:TransformBuilder.Clone +/ Local APT_PAGELET:TransformBuilder:MyCustomBuilder &b; &b = create APT_PAGELET:TransformBuilder:MyCustomBuilder(%This.ID); &b.Descr = %This.Descr; &b.LongDescr = %This.LongDescr; &b.Transformer = %This.Transformer.Clone(); &b.initializeSettings(%This.Settings.Clone()); Return &b; end-method;
Conclusion
Just like AWE, the Pagelet Wizard is a well-designed user configuration tool built entirely from common App Designer definitions. The design of the Pagelet Wizard, driven by application classes and metadata, makes it as extensible as it is flexible. In the hands of a functional expert, the delivered Pagelet Wizard is an effective tool for enhancing the user experience. The predefined data types, transformers, and display formats offer a variety of ways to select and format data. In the hands of a developer, the Pagelet Wizard offers unlimited possibilities. The material in this chapter just scratches the surface of the Pagelet Wizard’s extensibility. If you are interested in extending the Pagelet Wizard, review the application classes in the PTPPB_ PAGELET application package. In that package, you will find several well-written data types, transformers, and display formats. The Pagelet Wizard enables PeopleSoft users to rapidly create page fragments that rival the best designed pages of expert PeopleSoft developers. Given the ease of use and flexibility of the Pagelet Wizard, the next time a user asks you for a custom page to view transactional information, consider the Pagelet Wizard.
Notes 1. Oracle Corporation, Enterprise PeopleTools 8.49 PeopleBook: Security Administration, Securing Data with Pluggable Cryptography [online]; available from http://download .oracle.com/docs/cd/E13292_01/pt849pbr0/eng/psbooks/tsec/book.htm?File=tsec/htm/ tsec13.htm%23g037ee99c9453fb39_ef90c_10c791ddc07_633.
This page intentionally left blank
Part
II
Extend the User Interface
This page intentionally left blank
Chapter
5
Understanding and Creating iScripts
188
PeopleSoft PeopleTools Tips & Techniques
B
y now, you should be familiar with page and component development. Using App Designer, you can design a data bound page in true “what you see is what you get” (WYSIWYG) fashion, and then view that page at runtime through PeopleSoft’s Pure Internet Architecture (PIA). The PIA is responsible for every aspect of a component’s life cycle. It converts a design-time display into HTML and JavaScript, responds to user-initiated events, marshals data between the database and the display, and manages relationships between header and detail rows. iScripts offer an alternative to pages and components. They are similar to those items, in that you access them from a web browser, but that is where the similarities end. Unlike PeopleSoft page development, which uses drag and drop, iScript development requires coding. It is impossible to create an iScript without writing code. In this chapter, you will learn how to use iScripts to extend your browser’s feature set through bookmarklets, integrate with desktop applications such as Microsoft Outlook, and build rich user interfaces using Flex.
iScripts Defined
iScripts are the PeopleCode equivalent of Active Server Pages (ASP) or Java Server Pages (JSP). iScripts are PeopleCode functions that have access to the HTTP request and response entities through the PeopleCode Request and Response objects. iScripts provide full access to the PeopleSoft application database using standard PeopleCode data access objects and functions, as well as managed definitions like HTML definitions, SQL definitions, application classes, and FUNCLIBs. An iScript does not have a component processor. It has no event handlers. It does not have a page assembler. It doesn’t even have a component buffer or any sort of data binding architecture. What does an iScript have? Freedom! At runtime, a page created in App Designer is just a page. iScripts, on the other hand, can become whatever you want them to be. You can write an iScript that becomes a calendar appointment, a spreadsheet, or even synthesized speech! What’s more, an iScript may be a calendar appointment for one HTTP request and a spreadsheet the next. iScripts follow a few basic rules. By definition, iScripts are FUNCLIB functions, which means they are PeopleCode functions defined in record field PeopleCode. Just as with FUNCLIB functions, developers generally create iScripts in the FieldFormula event, but this is not required. iScripts differ from FUNCLIB functions in the following ways: ■■ An iScript must be defined in a record, known as WEBLIB whose name starts with WEBLIB. ■■ An iScript function name must start with IScript_. ■■ An iScript function has no parameters. ■■ An iScript function does not return a value. PeopleSoft uses the WEBLIB_ naming convention to differentiate WEBLIBs from standard FUNCLIBs. Also, PeopleSoft uses the IScript_ naming convention to differentiate private, internal functions from public iScript functions. This required naming convention presents a challenge to organizations that have naming convention standards for custom definitions. They cannot prefix custom web libraries with their custom identification text to differentiate delivered web libraries from custom libraries.
Chapter 5: Understanding and Creating iScripts
189
As you will see later in this chapter, iScripts gather parameters through the Request object and return a value through the Response object.
Our First iScript
We will start with the traditional Hello World example. Since iScripts are record field PeopleCode functions, create a new record in App Designer. Switch to the Record Type tab and change the type to derived/work. Next, we need to add a field to this record. PeopleSoft applications come with several iScript fields. You can use any field to identify an iScript. I generally use the field ISCRIPT1. For this example, insert the field ISCRIPT1 into the record definition and save the record. When prompted, name the record WEBLIB_APT_HW. Figure 5-1 shows the new WEBLIB record definition.
Coding the iScript After saving the record, switch to the record’s PeopleCode view and double-click the FieldFormula event. (As noted earlier, you can write iScript code in any event, but by convention, PeopleSoft developers use the FieldFormula event.) Type the following code into the PeopleCode editor and save the script: Function IScript_HelloWorld() %Response.Write("Hello World"); End-Function;
FIGURE 5-1. WEBLIB_APT_HW derived/work record definition
190
PeopleSoft PeopleTools Tips & Techniques
WEBLIB Naming Conventions When naming App Designer definitions, it is a best practice to use a site-specific prefix. For example, each definition in this book uses the APT_ prefix. This helps you, as the reader, differentiate between delivered definitions and the definitions created in this book’s examples. Similarly, a site-specific prefix helps developers differentiate between delivered definitions and custom definitions. Some definitions, like WEBLIBs, however, must conform to a naming convention that violates this site-specific recommendation. In this case, it is customary to place the sitespecific prefix after the PeopleTools required prefix. Therefore, WEBLIBs created in this book will always begin with WEBLIB_APT_. Unfortunately, this naming convention leaves only four characters for identifying the purpose of a WEBLIB. It is rare that you find a four-letter word that summarizes the purpose of a WEBLIB. With that said, expect very cryptic WEBLIB names. If I am not able to identify a four-letter word that describes a WEBLIB, I generally resort to an abbreviation.
Notice the code looks similar to any other FUNCLIB function. iScripts follow the same patterns and utilize the same concepts. This code listing introduces the %Response system variable. %Response is a system variable that is available to any online PeopleCode event. For example, you may see %Response.RedirectURL in a FieldChange event, but most of the Response object’s properties and methods are relevant only for iScripts. The Response object encapsulates methods and properties for generating an appropriate HTTP response to return to the client browser. Our iScript uses the Response object’s Write method to generate the HTTP response body. The Response object also includes methods and properties for setting headers, the return code, and even redirecting the user to a different page. For a complete listing of the Response object’s properties and methods, refer to the PeopleBooks PeopleCode API Reference.
Testing the iScript Before we can test this iScript, we need to grant execute permission to a permission list. Log in as a security administrator, navigate to PeopleTools | Security | Permissions & Roles | Permission Lists, and open the permission list APT_CUSTOM. Switch to the Web Libraries tab and add the web library WEBLIB_APT_HW. Click the Edit link beside the web library’s name to set permissions for each of the iScripts within that library. On the Weblib Permssions page, click the Full Access button. Click OK to accept the iScript permission changes, and then save the permission list. You can read more about web libraries and security in the Security Administration PeopleBook. Now you can execute this iScript. While logged in to a web browser with a user containing the role APT_CUSTOM, enter the following URL: http:///psp//EMPLOYEE/HRMS/s/WEBLIB_APT_HW.ISCRIPT1.FieldFormula .IScript_HelloWorld Replace with your PeopleSoft server name and with your PeopleSoft site name. When you load this URL, you should see a web page that resembles Figure 5-2.
Chapter 5: Understanding and Creating iScripts
191
FIGURE 5-2. Running the Hello World iScript Note Replacing psp with psc in the URL for this example will render the same iScript result, but with the PeopleSoft header and menu removed.
Modifying the iScript Browsers have default fonts for various document types. For example, when displaying HTML, your browser will use a serif font such as Times New Roman, and it will use a fixed-width font, like Courier, for plain text. Since our iScript prints plain text, we would expect the web browser to interpret it as such and display it with a fixed-width font. Notice, however, that the text is displayed using your browser’s default HTML font. Using your browser’s view source command, you can verify that the content returned by the iScript contains no HTML. Since the content isn’t telling the browser to display this text as HTML, something else is. The web server, in this case, is telling the browser how to interpret the iScript’s content.
192
PeopleSoft PeopleTools Tips & Techniques
A web server sends the Content-Type header as part of the HTTP response so that browsers appropriately interpret the response’s content. PeopleSoft defaults the Content-Type header to text/html. You can confirm this with an HTTP debugging proxy or packet sniffer like Fiddler or Wireshark. Let’s modify the iScript to set the header to a more appropriate content type, as follows: Function IScript_HelloWorld() %Response.SetContentType("text/plain"); %Response.Write("Hello World"); End-Function;
Reload the iScript in your browser. Did you notice the font change? Correctly setting HTTP headers can have a significant impact on the behavior of your browser.
A Bookmarklet to Call an iScript
With the basics of IScripts identified, let’s move onto a more practical example: the browser bookmarklet. A browser bookmark, sometimes referred to as a favorite, provides a mechanism for storing a URL. We typically think of bookmarks in relation to web page addresses, but bookmarks can store any type of URL. For example, you could create a bookmark to an e-mail address using the mailto protocol. A bookmarklet is a bookmark that uses the JavaScript protocol.1 You can think of a bookmarklet as a bookmark to a tiny JavaScript program. Through bookmark toolbar buttons, bookmarklets offer a means of extending the browser’s user interface. I first learned about bookmarklets from my friend and colleague Chris Heller. At OpenWorld, Chris presented a very practical iScript/bookmarklet example: using a bookmarklet to turn tracing on or off. I find that I regularly trace SQL to troubleshoot prompts, so a bookmarklet should help significantly. Before bookmarklets, this was my nine-step approach to tracing: 1. Navigate to the transaction that contained the prompt. 2. Click the new window link. 3. In the new window, navigate to PeopleTools | Utilities | Debug | Trace SQL. 4. Turn on tracing in that new window. 5. Switch back to the previous window, the transaction window. 6. Click the prompt. 7. Switch back to the trace window. 8. Turn off trace. 9. Review the trace file. Using the PeopleCode SetTraceSQL function, we can turn tracing on and off from an iScript. Using a bookmarklet, we can call this iScript from a browser toolbar button.
Chapter 5: Understanding and Creating iScripts
193
Writing the SetTraceSQL iScript Let’s define the iScript. Since WEBLIBs contain collections of iScripts, we could add this iScript to the previously defined WEBLIB record. However, because we may have multiple trace iScripts (SetTracePC, SetTraceSQL, and so on), it seems more appropriate to create a new record called WEBLIB_APT_DBG to store all of our trace functions. Just as with the Hello World iScript, our first step is to create a derived/work record with a single field named ISCRIPT1. Rather than create a new record, add the field, and so on, let’s clone the WEBLIB_APT_HW record by saving it as WEBLIB_APT_DBG. When prompted, do not copy the record’s PeopleCode. Note When you’re copying record definitions, App Designer will ask you if you want to save a copy of the source record’s PeopleCode. Unless you specifically want to copy the source record’s PeopleCode, say no. Saying yes will cause App Designer to loop through every event of every field (even if the record has no PeopleCode). Depending on the size of the record, this can take a considerable amount of time. After creating WEBLIB_APT_DBG, open the ISCRIPT1 FieldFormula PeopleCode event editor and add the following PeopleCode: Function IScript_SetSQLTrace Local number &level = Value(%Request.GetParameter("level")); SetTraceSQL(&level); %Response.Write("Trace level set to " | &level); End-Function;
This code introduces the %Request system variable. This variable is available to all online PeopleCode and represents the HTTP request received by the application server. The GetParameter method provides access to URL query string parameters as well as form post data. By creating the WEBLIB function with a level parameter, we make the function generic enough to turn tracing on and off. To turn on tracing, we call the iScript with a value greater than zero. To turn off tracing, we call the iScript with a value of zero. Add this new iScript to the APT_CUSTOM permission list and follow these steps to test it: 1. Navigate to http://hrms.example.com/psp/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_DBG .ISCRIPT1.FieldFormula.IScript_SetSQLTrace?level=7. 2. Look at the size of the log file in your App Server’s log file directory. 3. Click the home link at the top of the PeopleSoft application. 4. Recheck the trace file’s size (it should be larger). 5. Navigate to http://hrms.example.com/psp/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_DBG .ISCRIPT1.FieldFormula.IScript_SetSQLTrace?level=0. 6. Recheck the trace file’s size (it should be even larger). 7. Click the home link at the top of the PeopleSoft application. 8. Recheck the trace file’s size (the size should be the same as step 6).
194
PeopleSoft PeopleTools Tips & Techniques Tip You may see the text, “Authorization Error -- Contact your Security Administrator,” even after adding the iScript to the correct permission list. PeopleSoft displays this error message if you mistype the URL. For example, if you typed the field name as ISCRIPT instead of ISCRIPT1, PeopleSoft will display this authorization error message. Also, PeopleSoft URLs are case-sensitive. Therefore, you will see the same error message if you defined the function as IScript_SetSQLTrace, but called it with a lowercase i, as in iScript_SetSQLTrace.
Creating a Bookmarklet Now let’s write some JavaScript to call this URL. As you saw, we can call our SQL trace iScript from a standard URL, and, therefore, could use a regular bookmark. The problem with this approach is that activating a standard bookmark will cause the browser to navigate away from the transaction page. Instead, we will craft a JavaScript URL that opens the iScript in a new window. Here is the URL: javascript:(function(){window.open("/psc/hrms/EMPLOYEE/HRMS/s/ WEBLIB_APT_DBG.ISCRIPT1.FieldFormula.IScript_SetSQLTrace?level=7", "pstrace", "height=100,width=140,menubar=no,location=no,resizable=no, scrollbars=no,status=no");})();
Note The JavaScript for this example hard-codes the site, portal, and node names into the URL. Considering that you may use multiple sites, debug multiple portals, and run multiple applications, hard-coding these components of the URL is not a good idea. For example, along with the HRMS production system noted in this URL, you may also have HCMDMO, HCMDEV, HCMTST, and so on. In Chapter 6, we will rewrite this bookmarklet using JavaScript regular expressions to parse the site, node, and portal values from the current URL. Don’t worry about the details of this JavaScript. We will cover JavaScript in detail in Chapter 6. For now, it is enough to know that the window object has an open method, which has three parameters: URL, window name, and options. If you log in to PeopleSoft and type this URL into your browser, you will see the iScript open in a new window, as depicted in Figure 5-3. Before adding this bookmarklet to the browser’s toolbar, let’s embellish it a little. First, we will add a button to turn off tracing. Since adding a button will require some HTML, create a new App Designer HTML definition named APT_TRACE_BOOKMARKLET. Add the following HTML to APT_TRACE_BOOKMARKLET: Trace Bookmarklet body { font-family: Arial, sans-serif; font-size: small; }
Chapter 5: Understanding and Creating iScripts
195
FIGURE 5-3. Trace SQL bookmarklet
Trace level is now %Bind(:2)
This HTML contains two bind variables. The first parameter, %Bind(:1), is the iScript’s URL. Rather than hard-code the URL, as we did in the bookmarklet, we can use the GenerateIScriptContentURL PeopleCode function. The second parameter, %Bind(:2), is the new trace level. After turning off tracing, let’s have the pop-up window close automatically. This will require two steps. First, when the user clicks the Trace Off button, the browser will send a request to the trace iScript to turn off tracing. Next, the response from the PeopleSoft server will send a response containing JavaScript to close the pop-up window. The following code listing contains HTML and
196
PeopleSoft PeopleTools Tips & Techniques
JavaScript for closing the pop-up window. Save this HTML definition with the name APT_ TRACE_BOOKMARKLET_CLOSE. Trace Bookmarklet window.close(); You may close this browser window.
Next, let’s update the iScript’s PeopleCode. Here is the next iteration: Function Local Local Local
IScript_SetSQLTrace number &level = Value(%Request.GetParameter("level")); string &url; string &html;
/* This function accesses the database. We don't want this * function's database access to add to the size of the trace file. * Therefore, if &level is 0, then trace is on, so don't look up * values in the database until tracing is turned off. Otherwise, * look up all values before turning on tracing. */ If (&level = 0) Then SetTraceSQL(&level); &html = GetHTMLText(HTML.APT_TRACE_BOOKMARKLET_CLOSE); Else &url = GenerateScriptContentURL(%Portal, %Node, Record.WEBLIB_APT_DBG, Field.ISCRIPT1, "FieldFormula", "IScript_SetSQLTrace"); &html = GetHTMLText(HTML.APT_TRACE_BOOKMARKLET, &url, &level); SetTraceSQL(&level); End-If; %Response.Write(&html); End-Function;
Once you have a working bookmarklet, you may add it to your browser’s toolbar. The easiest way to get a bookmarklet onto a toolbar is to get the bookmarklet’s JavaScript into a hyperlink. Numerous sites offer bookmarklet creators that generate hyperlinks from bookmarklet URLs. I prefer the Bookmarklet Builder at http://www.subsimple.com/bookmarklets/jsbuilder.htm. After you have a bookmarklet in a hyperlink, you can use your mouse to drag the hyperlink to your browser’s bookmark/links toolbar. Figure 5-4 shows the new trace SQL pop-up window and my trace SQL toolbar button (in the upper-left corner).
Chapter 5: Understanding and Creating iScripts
197
FIGURE 5-4. Trace SQL bookmarklet window and toolbar button
For additional information about bookmarklets, take a look at http://www.bookmarklets.com and http://www.subsimple.com/bookmarklets/default.asp. The Subsimple site contains excellent tips and tools for building your own bookmarklets.
Desktop Integration
Using iScripts, we can generate files in a variety of file formats and serve those files to users. If we set the Response object’s HTTP headers appropriately, the user’s web browser will open the file with the correct desktop application. For example, RTF files (a file format used by Microsoft Word) are plain text files containing RTF format character sequences. Through careful inspection, it is possible to insert database values into an RTF file. Prior to XML Publisher, I combined RTF with XSL to generate mail-merge letters. Desktop spreadsheet programs such as Microsoft Excel or OpenOffice Calc have XML file formats that we can generate and serve from iScripts. Another productive desktop application integration point we can satisfy through iScripts is personal information management (PIM) system integration. Desktop applications such as Microsoft Outlook, Mozilla’s Thunderbird/Sunbird, and Yahoo! Zimbra allow users to store tasks, contacts, and events, as well as other important personal information. PeopleSoft applications
198
PeopleSoft PeopleTools Tips & Techniques
contain several areas of overlap with PIM applications. For example, CRM contains prospect contact information, HRMS contains recruiting interview schedules, and FSCM contains payables tasks. Using iScripts, we can integrate this information with the desktop PIM application of our choice. As an example, we will use iScripts to integrate calendar information.
Creating an iScript to Serve Calendar Content Many PeopleSoft transactions are date-centric. For example, payables have due dates, and interviews have schedule dates. It would be nice to have an automated way to add these events to your calendar. We could add these events to calendars using the server-side integration points offered by most enterprise calendar systems. As an alternative, we can use iScripts to create iCalendar files. iCalendar files are plain text files containing text conforming to IETF RFC 5545.2 The text of an iCalendar file describes an event in a format understood by most desktop calendar applications. Since iCalendar files contain plain text, we can generate and serve them from iScripts. Note IETF stands for Internet Engineering Task Force. RFC stands for Request for Comment. An IETF-published RFC is considered a standard. Therefore, IETF RFC 5545 is the standard defining iCalendar files. The following are the contents of a sample iCalendar file: BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PT Tips and Techniques//PeopleCode vCal 1.0//EN BEGIN:VEVENT DTSTART:20090814T160000Z DTEND:20090814T180000Z SUMMARY:Interview Stewart Johannsen DESCRIPTION:Interview Stewart Johannsen with Vicky Adler LOCATION:Conf Rm 1 CATEGORIES:Business,Interviews,Administration END:VEVENT END:VCALENDAR
We will create an iScript to serve this content. First, place the iCalendar content above in an HTML definition named APT_ICAL_TEMPLATE. Next, create a derived/work record named WEBLIB_APT_CAL. Add the field ISCRIPT1 to this record, and then add the following PeopleCode to the FieldFormula event: Function IScript_Calendar %Response.SetContentType("text/calendar"); %Response.SetHeader("Content-Disposition", "attachment;filename=PSEvent.ics"); %Response.Write(GetHTMLText(HTML.APT_ICAL_TEMPLATE)); End-Function;
Chapter 5: Understanding and Creating iScripts
199
The first line of this function tells the browser that this is an iCalendar file that should be rendered by a calendar program, rather than by a web browser. When downloading dynamically generated files from the Internet, unless instructed otherwise, your browser will use the name of the program that generated the file. For example, when generating files from the calendar iScript, the browser will suggest the name WEBLIB_APT_CAL.ISCRIPT1 .FieldFormula.IScript_Calendar. Using the Content-Disposition header allows us to provide a user-friendly name for the downloaded file. Besides providing a friendly name, the Content-Disposition header assists the browser in determining how to handle the iScript’s response. Some file types don’t have registered MIME content handlers. If your computer does not have an application registered to handle the text/ calendar MIME type, your browser will look for a default file type handler using the file’s extension. Add this WEBLIB to the APT_CUSTOM permission list and call it from the URL http://hrms .example.com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_CAL.ISCRIPT1.FieldFormula.IScript_ Calendar. Your browser should respond by prompting you to open or save the downloaded file. If you have a modern calendar software program, such as Microsoft Outlook 2007, saving this calendar entry will automatically add it to your calendar. Figure 5-5 is a screenshot of my Mozilla Firefox browser suggesting that I open this calendar file with Microsoft Outlook.
FIGURE 5-5. iCalendar file download prompt
200
PeopleSoft PeopleTools Tips & Techniques Note Client application support for the iCalendar format varies widely by application and version. The screenshots in this chapter utilize Microsoft Outlook 2007, which supports multiple-event iCalendar files.
Now let’s make this iCalendar dynamic by replacing each of the values in the template HTML definition with an HTML bind variable. Update your copy of the APT_ICAL_TEMPLATE file with the following (basically, just replace each value with a bind variable of the form %Bind(:n)): BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PT Tips and Techniques//PeopleCode vCal 1.0//EN BEGIN:VEVENT DTSTART:%Bind(:1) DTEND:%Bind(:2) SUMMARY:%Bind(:3) DESCRIPTION:%Bind(:4) LOCATION:%Bind(:5) CATEGORIES:%Bind(:6) END:VEVENT END:VCALENDAR
Note If you plan to generate iCalendar files in batch mode, store your template in a message catalog definition rather than an HTML definition. HTML definitions are available only to online PeopleCode. For a more robust templating solution, see the discussion of templating PeopleCode in Chapter 9.
Building a Parameter Cache We need to add the HTML definition’s bind variables to the iScript. Also, we need to parameterize the iScript so that it doesn’t always serve the same iCalendar event. There are a few ways to do this: ■■ Specify all values as query string parameters. This method turns the iScript into a formatting web service. The iScript reads data from the request and marks it up in iCalendar format. ■■ Specify transaction keys as query string parameters. This reduces the amount of data sent to the iScript, but tightly couples the iScript to a specific transaction. ■■ Use a parameter cache. With a parameter cache, we create a record to hold all of the parameters required by this iScript. We then populate the record from the originating transaction and add the cache ID to the iScript’s URL as a query string parameter. Specifying all the parameters in a query string won’t work for us, because the length of the parameters can exceed the maximum length of a URL. Additionally, since we may use this same
Chapter 5: Understanding and Creating iScripts
201
iScript for multiple transactions within a PeopleSoft application, it’s best to keep the iScript transaction-agnostic. Therefore, we will take the parameter cache approach. The cache pattern consists of PeopleCode in the source transaction that inserts values into a cache and then supplies the user with a link to this iScript. The link will need to uniquely identify the row in the cache record. We can use a Globally Unique Identifier (GUID) to provide uniqueness. (A GUID is a system-generated character sequence commonly used as a unique identifier.) Our cache consists of a single record definition that stores parameters by row, with each row identified by a GUID. Let’s create the cache record now. Name it APT_CAL_CACHE. This record is defined as a standard SQL table. Figure 5-6 shows the record’s fields. Be sure to make the GUID field a key field. After adding all of the fields, save and build the record. Next, we will modify the iScript function to read a GUID from the %Request object and then populate the iCalendar bind variables from values in the cache table. Replace the IScript_Calendar function you created earlier with the following PeopleCode: Function Local Local Local Local
IScript_Calendar Record &cache = CreateRecord(Record.APT_CAL_CACHE); string &dtstart; string &dtend; datetime &tempTime;
FIGURE 5-6. iCalendar cache record definition
202
PeopleSoft PeopleTools Tips & Techniques &cache.GUID.Value = %Request.GetParameter("id"); If (&cache.SelectByKey()) Then %Response.SetContentType("text/calendar"); %Response.SetHeader("Content-Disposition", "attachment;filename=PSEvent.ics"); REM ** Format dates; &tempTime = DateTimeToTimeZone(&cache.START_DTTM.Value, "Local", "UTC"); &dtstart = DateTimeToLocalizedString(&tempTime, "yyyyMMdd'T'HHmmss'Z'"); &tempTime = DateTimeToTimeZone(&cache.END_DTTM.Value, "Local", "UTC"); &dtend = DateTimeToLocalizedString(&tempTime, "yyyyMMdd'T'HHmmss'Z'");
%Response.Write(GetHTMLText(HTML.APT_ICAL_TEMPLATE, &dtstart, &dtend, &cache.DESCR.Value, &cache.DESCR254.Value, &cache.LOCATION_DESCR.Value, &cache.CATEGORY.Value)); &cache.Delete(); Else %Response.Write("Event not found. Please revisit the " | "original transaction"); End-If; End-Function;
Since a user may access the same transaction multiple times, and each access will insert a row into the parameter cache, this iScript deletes the cache entry after serving it to the browser. Note The previous code listing uses the UTC time zone. Navigate to PeopleTools | Utilities | International | Time Zones3 and make sure your system has a UTC time zone. UTC is similar to GMT, but PeopleSoft’s GMT metadata observes daylight saving time (DST). The iCalendar specification requires dates be converted to UTC. Later, we will add some PeopleCode to a transaction to generate a link to this iScript. For now, let’s use SQL to insert a value into the cache so we can test this iScript. Since GUIDs consist of cryptic 36-character string, we won’t want to type one into the SQL insert and then again into a URL. Instead, let’s use our own identifier: CACHE_TEST. Use the following SQL statements to populate the cache. For Oracle, use this SQL: INSERT INTO PS_APT_CAL_CACHE VALUES('CACHE_TEST' , TO_DATE('2010-07-23 09:00', 'YYYY-MM-DD HH24:MI') , TO_DATE('2010-07-23 11:00', 'YYYY-MM-DD HH24:MI')
Chapter 5: Understanding and Creating iScripts , , , ,
203
'Interview Stewart (cache)' 'Interview Stewart Johannsen with Vicky Adler' 'Conf Rm 1' 'Inter')
For SQL Server, use this SQL: INSERT INTO PS_APT_CAL_CACHE VALUES('CACHE_TEST' , '2010-07-23 09:00' , '2010-07-23 11:00' , 'Interview Stewart (cache)' , 'Interview Stewart Johannsen with Vicky Adler' , 'Conf Rm 1' , 'Inter')
Load this iScript in your browser. You should see an iCalendar entry that is similar to the one you saw with the previous iScript. However, we changed the description so we could see that this file came from the APT_CAL_CACHE record. Here is the iScript’s URL with the new ID query string parameter: ttp://hrms.example.com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_CAL.ISCRIPT1 h .FieldFormula.IScript_Calendar?id=CACHE_TEST Tip You may want to comment out the &cache.Delete() statement in the iScript until you have the iScript working successfully.
Modifying the Transaction It is time to integrate a transaction with our scheduling system. Using the delivered HRMS demo data, I scheduled an interview, as shown in Figure 5-7. Let’s add a download link just to the right of the Location column in the Interview Schedule grid. To add the download link, we will need a derived/work record and some PeopleCode. To reduce the impact of this customization, we will add the PeopleCode to the derived/work record rather than the component. The download button will need a field for its PeopleCode. Create a new field named APT_ DOWNLOAD_PB, as shown in Figure 5-8. Next, create a derived/work record and add the APT_DOWNLOAD_PB field to this new record, as shown in Figure 5-9. Save the record as APT_INTERVW_WRK. With our supporting definitions created, we can add a new hyperlink to the interview page. Open the page HRS_INT_SCHED. Scroll down to the Interview Schedule grid and add a new button as the last column in the grid. Double-click the new button to reveal its properties and change the button’s type to Hyperlink. Set the button’s record name to APT_INTERVW_WRK and its field name to APT_DOWNLOAD_PB. Check Enable When Page is Display Only. On the Label tab, change the Type to Text. Figure 5-10 is a partial screenshot of the modified page in App Designer. Notice the Add to Calendar column on the right within the grid. Save the page.
204
PeopleSoft PeopleTools Tips & Techniques
FIGURE 5-7. Interview schedule before customization
We will add PeopleCode after reviewing the visual appearance of the page. To view this page online, navigate to Self Service | Recruiting Activities | Interview Team Schedule. Since I am using the delivered HRMS demo database, I logged in as HCRUSA_KU0011. On the Interview Team Schedule page, select the View Schedule link next to any of the job openings. The next step is to make the hyperlink active by adding some FieldChange PeopleCode. Open the APT_INTERVW_WRK record, and then open the APT_DOWNLOAD_PB FieldChange PeopleCode event editor. Add the following PeopleCode: Local Local Local Local
Record &cache = CreateRecord(Record.APT_CAL_CACHE); string &guid; string &url; string &dateStr = DateTimeToLocalizedString( HRS_INT_SCHED.HRS_INT_DT, "MM/dd/yyyy");
Chapter 5: Understanding and Creating iScripts
FIGURE 5-8. New APT_DOWNLOAD_PB field definition
FIGURE 5-9. APT_INTERVW_WRK record definition
205
206
PeopleSoft PeopleTools Tips & Techniques
FIGURE 5-10. HRS_INT_SCHED page online
/* GUID generation alternatives */ REM ** Generate a GUID on Oracle DB on PT version below 8.49; REM SQLExec("SELECT RAWTOHEX(SYS_GUID()) FROM PS_INSTALLATION", &guid); REM ** Generate a GUID on Microsoft SQL Server REM SQLExec("SELECT newid()", &guid); REM ** Generate a GUID with PT 8.49 or higher (Java 1.5 or higher); &guid = GetJavaClass("java.util.UUID").randomUUID().toString(); &cache.GUID.Value = &guid; &cache.START_DTTM.Value = DateTimeValue(&dateStr | HRS_INT_SCHED.HRS_START_TM);
Chapter 5: Understanding and Creating iScripts
207
&cache.END_DTTM.Value = DateTimeValue(&dateStr | HRS_INT_SCHED.HRS_END_TM); &cache.DESCR.Value = "Interview " | HRS_APP_NAME_I.NAME_DISPLAY; &cache.DESCR254.Value = "You are scheduled to interview " | HRS_APP_NAME_I.NAME_DISPLAY | " for the " | HRS_JOBCODE_I.DESCR | " position."; &cache.CATEGORY.Value = "Interview"; &cache.Insert(); &url = GenerateScriptContentURL(%Portal, %Node, Record.WEBLIB_APT_CAL, Field.ISCRIPT1, "FieldFormula", "IScript_Calendar") | "?id=" | &guid; ViewContentURL(&url);
This code copies selected transaction values into our iCalendar cache record using a GUID as a key. Our first dilemma is determining how to create a GUID.4 Since PeopleBooks does not contain documentation for a GUID generation function, we will turn to the PeopleTools infrastructure. From the operating system to the database, we have several options for creating a GUID. The preceding code presents two of those options: SQL or Java. If you are using PeopleTools 8.49 or higher, you can take advantage of the database and platform-independent GUID-generation capabilities provided by Java 1.5. If you are using an earlier version of PeopleTools, you can call on the strength of your database to generate a GUID. This example contains GUID-generation SQL for Oracle and SQL Server. If neither of these fits your environment, take a look at the Apache Commons Id5 project, which provides a UUID object that is very similar to the Java 1.5 UUID object. Note Java offers a variety of ways to extend PeopleCode. Part III of this book is devoted to Java. Reload the Interview Schedule page and test the new Add to Calendar hyperlink. When you click this link, your browser should prompt you to open or save a file named PSEvent.ics. Figure 5-11 shows the Interview Schedule page with a browser download prompt, and Figure 5-12 shows that same interview in Microsoft Outlook.
208
PeopleSoft PeopleTools Tips & Techniques
FIGURE 5-11. Interview Schedule page with download prompt
Note If you receive an authorization error after clicking the Add to Calendar hyperlink, verify that your logged-in user is a member of the APT_ CUSTOM role (or a member of another role that has access to the IScript_Calendar function from the WEBLIB_APT_CAL web library). The interview scenario presented here is just one example of using iScripts to integrate with PIM systems. You can use the same approach to integrate CRM contacts as vCards or ESA action items as VTODO’s (to-do’s).
Chapter 5: Understanding and Creating iScripts
209
FIGURE 5-12. Interview details in Microsoft Outlook
Serving File Attachments
In the next section of this chapter, we are going to incorporate Adobe Flex with PeopleSoft pages. Flex uses the binary SWF file format. We need a way to store those binary files and then serve them upon request. If you have access to your web server, I recommend storing static content, such as Flex SWF files, on your web server. If you don’t have access to your web server’s file system, you can use the Web Assets component we created in Chapter 2. Before you can use the Web Assets component, however, you will need to create a method to serve assets, just as the web server would if the files were stored on the web server’s file system. Through customizations in Chapter 3, we added an attachment record and an approval record to our Web Assests component. We will need an SQL statement to join these three records
210
PeopleSoft PeopleTools Tips & Techniques
so we only select approved web assets that have attachments. Create a new SQL definition in App Designer. Name it APT_APPROVED_ASSET_DETAILS and add the following SQL: SELECT , FROM INNER ON INNER ON WHERE AND
A.MIMETYPE B.ATTACHSYSFILENAME PS_APT_WEB_ASSETS A JOIN PS_APT_WA_ATTACH B A.ASSET_ID = B.ASSET_ID JOIN PS_APT_WA_APPR C A.ASSET_ID = C.ASSET_ID C.APPR_STATUS = 'A' A.ASSET_ID = :1
Next, create a new derived/work record that contains the field ISCRIPT1. Name this new record WEBLIB_APT_WA. Open the ISCRIPT1 FieldFormula event and add the following PeopleCode: Function Local Local Local Local Local
IScript_GetWebAsset() any &data; string &id = %Request.GetParameter("id"); string &file_name; string &content_type; SQL &cursor;
SQLExec(SQL.APT_APPROVED_ASSET_DETAILS, &id, &content_type, &file_name); %Response.SetContentType(&content_type); &cursor = CreateSQL("SELECT FILE_DATA FROM PS_APT_WA_ATTDET " | "WHERE ATTACHSYSFILENAME = :1 ORDER BY FILE_SEQ", &file_name); While &cursor.Fetch(&data); %Response.WriteBinary(&data); End-While; End-Function;
As always, add this new WEBLIB to the APT_CUSTOM permission list before testing the iScript. Here is the URL I used to test this iScript: http://hrms.example.com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_WA.ISCRIPT1 .FieldFormula.IScript_GetWebAsset?id=SUBMIT_TEST When adding workflow to the Web Assets component in Chapter 3, I created an asset with the ID SUBMIT_TEST, which is the ID that I passed to the iScript as a query string parameter. Replace this value with an approved asset from your Web Asset’s component.
iScripts as Data Sources
Rich Internet technologies change the way we interact with web-based applications. PeopleTools 8.4 used the traditional, highly visible HTTP Request/Response cycle to implement web-based applications. Using this model, a FieldChange event or prompt submitted the entire page to
Chapter 5: Understanding and Creating iScripts
211
the PeopleSoft server, requiring the user to wait for the browser to “repaint” the page. The rich Internet approach, on the other hand, uses transparent mechanisms to send HTTP requests to the web server, process the web server’s response, and then update the page accordingly. PeopleTools 8.5 uses AJAX to implement this rich experience. AJAX makes extensive use of iScripts. AJAX is the subject of Chapter 7. Here, we will cover another rich Internet technology: Adobe’s Flex. Because iScripts allow you to define the structure of the returned data without the additional HTML generated by the component processor and the page assembler, iScripts make excellent data source providers. For example, Adobe’s Flex allows programmers to create rich user interfaces that read from and write to web resources. We can use iScripts to implement those web resources.
Flex Requirements Flex is a popular technology for generating rich Internet applications. It uses the ubiquitous Adobe Flash Player to display rich components. By combining Flex with iScripts, we can create a rich user experience bound to PeopleSoft data. Flex requires the following tools: ■■ Text editor ■■ Flex SDK command-line compiler ■■ The latest copy of Adobe’s Flash Player You can download the open source Flex SDK, including the Flex command-line compiler, from http://opensource.adobe.com. Installation is a matter of expanding the zip file into the directory of your choice. For convenience, I recommend adding the Flex SDK bin directory to your PATH environment variable. Flex programs are defined in XML, which means you can edit them with any text editor. PeopleSoft developers use text editors to create SQR programs, modify COBOL programs, or even write database-specific T-SQL and PL/SQL programs. Even though you can use any text editor, I recommend downloading one of the syntax-highlighting text editors, such as jEdit (my preference), Textpad, or Notepad++. Each of these text editors has syntax highlighting for the common PeopleSoft text file formats: SQR, COBOL, SQL, and XML. In addition to syntax highlighting, jEdit has a variety of plug-ins to support common development tasks. You can find a jEdit Data Mover and PeopleCode syntax file, as well as all my public jEdit syntax files, at http://www.box.net/jimjmarion. If you are interested in a full-featured Flex development environment, you may want to try Adobe’s Flex Builder. For a middle-of-the-road approach that balances cost (free) with productivity, you can configure Eclipse to use an XML schema for code completion and Apache Ant for build support. While this solution is free, it may not be as productive as Adobe’s Flex Builder. For the examples in this chapter, we will use a standard text editor and the Flex command-line compilers.
Say Hello to Flex Let’s start with a Hello World sample application. In Chapter 1, we created the UserGreeter application class. We will create a Flex component that fetches its data from a PeopleSoft iScript. Figure 5-13 is a screenshot of what we are trying to create. The box in the center of the page containing the text “Hello [PS] PeopleSoft Superu…” is a Flex component. The Flex component is static, but the text is dynamic. The greeting “Hello…” actually comes from an iScript, and shows a different value for each logged-in user.
212
PeopleSoft PeopleTools Tips & Techniques
FIGURE 5-13. PeopleSoft page with a Flex component
Creating SayHello.mxml Using your favorite text editor, create a text file named SayHello.mxml and add the following contents:
Open a command window (type cmd.exe at the Windows Run prompt) and navigate to the directory containing your text file (cd ). Compile this text file into a SWF file using the command line: mxmlc -keep-generated-actionscript=true -incremental=true SayHello.mxml
If you added the Flex SDK bin directory to your PATH environment variable, you can issue the mxmlc command directly from the command line. If you didn’t, then you will need to use the full path to mxmlc. For example, if you installed the Flex SDK at C:\flex_sdk_3, you would issue this command: C:\flex_sdk_3\bin\mxmlc -keep-generated-actionscript=true -incremental=true SayHello.mxml
Check the compiler’s output for errors and resolve any identified errors before continuing. Tip For build automation, try Apache Ant. Flex fully integrates with Ant through custom Ant tasks. This allows you to create reusable project build files. As you know, development is iterative. This means you will recompile the same file several times. Ant’s build system automates mundane operating system tasks such as compiling, copying files, and deploying files. Many full-featured development environments offer Apache Ant integration. JDeveloper, Eclipse, and even jEdit offer full Ant integration, as well as XML syntax highlighting and formatting.
214
PeopleSoft PeopleTools Tips & Techniques
Uploading the SWF File The mxmlc Flex compiler used SayHello.mxml to generate SayHello.swf. Now we need to move this file to a location where our PeopleSoft users can download it as part of a standard PeopleSoft page request. If you have access to your PeopleSoft web server, you will achieve the best performance by placing SayHello.swf in a folder on your PeopleSoft web server. This allows your web browser to take advantage of the web server’s cache headers. Caution Before using your PeopleSoft web server for anything beyond what is delivered by PeopleSoft, make sure you are familiar with your license agreement. Serving additional files, such as SWF files, may require a full web server license, rather than the restricted use license included with PeopleSoft applications. Assuming you don’t have access to your PeopleSoft web server’s file system, we will use the Web Assets component from Chapter 2 and the iScript we created earlier in this chapter to serve SayHello.swf. Using your web browser, log in to PeopleSoft and navigate to PeopleTools | Utilities | Administration | Web Assets. Add the new value APT_FLEX_HW, as shown in Figure 5-14.
FIGURE 5-14. APT_FLEX_HW web asset definition
Chapter 5: Understanding and Creating iScripts
215
Notice the Mime-Type field is set to application/x-shockwave-flash and the attachments group box contains the file SayHello.swf. Before you try downloading this new web asset, I suggest that you modify the APT_APPROVED_ ASSET_DETAILS SQL definition. As written, the SQL requires you to approve a web asset before you can serve it. As you recall from Chapter 3, once a web asset is approved, it can’t be modified. You may find yourself recompiling and reuploading SayHello.swf several times as you resolve issues. To make this process simpler, comment out C.APPR_STATUS = 'A' in the APT_ APPROVED_ASSET_DETAILS SQL definition. By making this change, you will be able to modify and test the same web asset multiple times. If you decide not to change the SQL, be sure to run through the web asset approval cycle before continuing. You can verify your web browser loads this web asset correctly using this URL: http://hrms.example.com/psp/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_WA.ISCRIPT1 .FieldFormula.IScript_GetWebAsset?id=APT_FLEX_HW Figure 5-15 shows the new SayHello Flex web asset. In the next step, we will write PeopleCode to provide real-time data to the Flex HTTPService.
FIGURE 5-15. Downloaded APT_FLEX_HW Web Asset
216
PeopleSoft PeopleTools Tips & Techniques
Creating the Say Hello HTTPService iScript The SayHello Flex definition includes an HTTPService element. The intent is to create a static SWF file that reads dynamic content from the PeopleSoft application. We can provide data to Flex using iScripts or Integration Broker. Since we are using Flex in the context of a PeopleSoft application, we can reuse the existing PeopleSoft authenticated session by serving data from an iScript. We want our iScript to serve XML that has a certain format. Using the template concept demonstrated earlier, we can use an HTML definition to create an XML template for this iScript. Create a new HTML definition with the name APT_SAY_HELLO_TEMPLATE and add the following code: %Bind(:1)
To define the SayHello HTTPService iScript, use App Designer to create a new derived/ work record. Add the field ISCRIPT1 and save the record as WEBLIB_APT_HI. Figure 5-16 shows this new WEBLIB in App Designer. Next, switch to the record’s PeopleCode display and open the ISCRIPT1 FieldFormula event PeopleCode editor. Add the following code to this event: import APT_GREETERS:UserGreeter; Function IScript_SayHelloService Local APT_GREETERS:UserGreeter &greeter = create APT_GREETERS:UserGreeter(); Local string &message = &greeter.sayHello(); Local string &xml = GetHTMLText(HTML.APT_SAY_HELLO_TEMPLATE, &message); %Response.SetContentType("text/xml"); %Response.WriteLine(&xml); End-Function;
FIGURE 5-16. WEBLIB_APT_HI Derived/Work record definition
Chapter 5: Understanding and Creating iScripts
217
As always, after creating the iScript, be sure to add it to the APT_CUSTOM permission list. You can test this new iScript by navigating to this URL: http://hrms.example.com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_HI.ISCRIPT1 .FieldFormula.IScript_SayHelloService Logged in as demo user PS, I get the following results: Hello [PS] Peoplesoft Superuser
Supporting App Designer Definitions Besides fetching data from PeopleSoft, we want to embed Flex content in a PeopleSoft page. To do this, we need to create the following objects in App Designer: ■■ An HTML definition that will contain JavaScript and HTML for inserting the SWF file into a PeopleSoft page ■■ A derived/work record used to add the HTML definition to a page ■■ A PeopleSoft page ■■ A PeopleSoft component Create a new HTML definition named APT_SAY_HELLO_FLEX and add the following HTML. Note This HTML contains a few lines of JavaScript (covered in Chapters 6 through 8), which uses the Google AJAX Libraries API (http://code .google.com/apis/ajaxlibs/) to serve the SWFObject JavaScript library. I highly recommend using the SWFObject JavaScript library to simplify embedding SWF files in HTML, but do have some concerns about the security ramifications of allowing a third-party site to inject 6 uncontrolled JavaScript into highly confidential PeopleSoft pages. As an alternative to using the Google AJAX Libraries API, you can download the SWFObject JavaScript library (http://code.google .com/p/swfobject/) and then place it on your web server, serve it from a JavaScript HTML definition, or serve it as a web asset. Chapter 6 explains these options in greater detail. function getServiceURL() { return "%Bind(:1)"; }
218
PeopleSoft PeopleTools Tips & Techniques
Your browser does not support Flash
(function() { var params = { allowscriptaccess: "always" }; swfobject.embedSWF("%Bind(:2)", "APT_flashContent", "250", "150", "9.0.0", "", false, params); })();
The HTML definition shown in this code listing contains bind variables for the SWF file’s location and the iScript HTTPService location. Rather than hard-code these URLs, we will use the PeopleCode GenerateScriptContentURL function to create URLs at runtime. This ensures that the URLs point to the correct servers as you promote your code from DEV to TST to PRO. HTML that is generated at runtime must be associated with a derived/work record. Create a new derived/work record named APT_FLEX_HI_WRK and add the field HTMLAREA, as shown in Figure 5-17. With all the supporting definitions created, we can build a new PeopleSoft page for the Flex content. Create a new page named APT_FLEX_SAY_HELLO. Add an HTML Area control to the top of the page and set its properties as follows: ■■ Value: Field ■■ Record Name: APT_FLEX_HI_WRK ■■ Field Name: HTMLAREA
FIGURE 5-17. APT_FLEX_HI_WRK Derived/Work record definition
Chapter 5: Understanding and Creating iScripts
219
FIGURE 5-18. HTML Area properties Figure 5-18 shows the HTML Area control’s properties. You can see the HTML Area control on the page in the background. Save this page after configuring the HTML Area control. Before we can view this example online, we need to create a component and register it with the PeopleTools portal. Create a new component named APT_FLEX_SAY_HELLO. Add the APT_FLEX_SAY_HELLO page to this component and set the search record to INSTALLATION. Save this component and register it using the following details: ■■ Menu: APT_CUSTOM ■■ Folder: PT_ADMINISTRATION ■■ Content reference label: Say Hello ■■ Long description: Say Hello from Flex ■■ Permission list: APT_CUSTOM Be sure to update the new menu item’s label after registration. For our final step before testing this example, we will add PeopleCode to the APT_FLEX_ SAY_HELLO component. Open the APT_FLEX_SAY_HELLO component and switch to the Structure tab. Navigate to Scroll - Level 0 | APT_FLEX_HI_WRK. Highlight this item in the outline,
220
PeopleSoft PeopleTools Tips & Techniques
right-click the item, and then select View PeopleCode from the context menu. The RowInit event editor should appear. Add the following PeopleCode to this event: Local string &swfUrl = GenerateScriptContentURL(%Portal, %Node, Record.WEBLIB_APT_WA, Field.ISCRIPT1, "FieldFormula", "IScript_GetWebAsset") | "?id=APT_FLEX_HW"; Local string &serviceUrl = GenerateScriptContentURL(%Portal, %Node, Record.WEBLIB_APT_HI, Field.ISCRIPT1, "FieldFormula", "IScript_SayHelloService"); APT_FLEX_HI_WRK.HTMLAREA = GetHTMLText(HTML.APT_SAY_HELLO_FLEX, &serviceUrl, &swfUrl);
Save the component and test it by navigating to PeopleTools | Utilities | Administration | Say Hello. You should see a page that looks like Figure 5-13, shown earlier in this chapter.
Direct Reports DisplayShelf Let’s take the concepts learned thus far and apply them to something a little more practical. In this next example, we will combine a custom Flex component with an iScript and a PeopleSoft page to create the Apple iTunes Cover Flow7 effect to display employee photos. In Chapter 7, we will add AJAX to show employee attributes when a user selects an image. Figure 5-19 is a screenshot of the PeopleSoft/Flex component we will create. The 3D Cover Flow effect is provided by the sample DisplayShelf ActionScript component. You can download the DisplayShelf’s source files from the blog post: http://www.quietlyscheming.com/blog/components/tutorial-displayshelf-component/
Creating the Employee Photo iScript This example will require an iScript to serve employee photos. Create a new derived/work record containing the field ISCRIPT1. Save this record with the name WEBLIB_APT_EPIC. Note The WEBLIB name WEBLIB_APT_EPIC is not to be confused with Epoch. Combined, the PeopleTools required WEBLIB prefix and the typical site specific prefix do not leave many characters available for describing the intent of a WEBLIB. The EPIC in the record name is actually an abbreviation for Employee PICture. Open the ISCRIPT1 FieldFormula PeopleCode event editor and add the following PeopleCode. This code listing selects image data from the HR employee photo table. It assumes those photos are stored in JPEG format. Notice that it contains the query string parameter EMPLID. Function IScript_EmployeePhoto Local string &emplid = %Request.GetParameter("EMPLID"); Local any &data; SQLExec("SELECT EMPLOYEE_PHOTO FROM PS_EMPL_PHOTO " | "WHERE EMPLID = :1", &emplid, &data); %Response.SetContentType("image/jpeg"); %Response.WriteBinary(&data); End-Function;
Chapter 5: Understanding and Creating iScripts
221
FIGURE 5-19. Employee photos Cover Flow effect
Caution The iScript to serve employee photos accepts an employee’s ID and then returns information about that employee without first verifying that the requesting user has access to the requested employee’s information. Depending on the nature of the requested information, this may present a security risk. When building your own iScripts, be sure to verify that the user has access to the requested information. After adding this iScript to the APT_CUSTOM permission list, you can test it with this URL: http://hrms.example.com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_EPIC.ISCRIPT1 .FieldFormula.IScript_EmployeePhoto?EMPLID=KU0054
222
PeopleSoft PeopleTools Tips & Techniques
ImageCoverFlow.mxml Source Code Just as we did with the SayHello Flex example, we need to create a Flex MXML file. Create a new text file and name it ImageCoverFlow.mxml. Add the following Flex code to this new text file:
After creating the ImageCoverFlow.mxml file and downloading the DisplayShelf source code, place the DisplayShelf.as and TiltingPanel.as files in the same directory as the ImageCoverFlow.mxml file. Compile the ImageCoverFlow.mxml file from the command line using the following command: mxmlc -keep-generated-actionscript=true -incremental=true ImageCoverFlow.mxml
Just as you did with the SayHello example, resolve any compile errors before continuing.
Chapter 5: Understanding and Creating iScripts
223
Uploading ImageCoverFlow.swf Using your web browser, log in to PeopleSoft and navigate to PeopleTools | Utilities | Administration | Web Assets. Add the new value APT_IMG_CF, as shown in Figure 5-20. Be sure to attach the ImageCoverFlow.swf file to the web asset. Note You could test this web asset using the URL http://hrms.example .com/psc/hrms/EMPLOYEE/HRMS/s/WEBLIB_APT_WA.ISCRIPT1 .FieldFormula.IScript_GetWebAsset?id=APT_IMG_CF, just as we did with the SayHello example. This time, however, the Flex component has a white background color. Therefore, until we provide a data service, the Image Cover Flow web asset will not be visible.
Writing the Image URL HTTPService iScript The DisplayShelf component needs a list of image URLs. It will load these images into the user interface at runtime. Using an iScript, we can generate a list of a manager’s employees by EMPLID and use the employee photo iScript we created earlier to serve photos to the DisplayShelf component.
FIGURE 5-20. APT_IMG_CF web asset definition
224
PeopleSoft PeopleTools Tips & Techniques
We will start with an SQL statement that selects a supervisor’s direct reports. Create an SQL statement named APT_SUPERVISOR_DIRECTS and add the following SQL: SELECT FROM INNER ON WHERE SELECT FROM WHERE AND AND AND SELECT FROM WHERE AND AND AND
J.EMPLID PS_JOB J JOIN PS_EMPL_PHOTO P P.EMPLID = J.EMPLID J.EFFDT = ( MAX(J_ED.EFFDT) PS_JOB J_ED J_ED.EMPLID = J.EMPLID J_ED.EMPL_RCD = J.EMPL_RCD J_ED.EFFDT