#1: Add HandleScreen() function as a higher-level interface to ShowScreen()

This commit is contained in:
Matthew R. Wilson committed 2020-12-07 01:59:04 -08:00
1 parent 1e6ae603e7
commit fee74affa2
5 files changed
+434 -2

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
*.exe
example/example
example1/example1
example2/example2
File renamed without changes.
+155
View File
@@ -0,0 +1,155 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
package main
import (
"fmt"
"net"
"os"
"strings"
"github.com/racingmars/go3270"
)
func init() {
// put the go3270 library in debug mode
go3270.Debug = os.Stderr
}
// A Screen is an array of go3270.Field structs. We will build two screens,
// a query screen and a result screen:
var screen1 = go3270.Screen{
{Row: 0, Col: 27, Intense: true, Content: "3270 Example Application"},
{Row: 2, Col: 0, Content: "Welcome to the go3270 example application. Please enter your name."},
{Row: 4, Col: 0, Content: "First Name . . ."},
{Row: 4, Col: 19, Name: "fname", Write: true, Highlighting: go3270.Underscore},
{Row: 4, Col: 40}, // field "stop" character
{Row: 5, Col: 0, Content: "Last Name . . . ."},
{Row: 5, Col: 19, Name: "lname", Write: true, Highlighting: go3270.Underscore},
{Row: 5, Col: 40}, // field "stop" character,
{Row: 6, Col: 0, Content: "Password . . . ."},
{Row: 6, Col: 19, Name: "password", Write: true, Hidden: true},
{Row: 6, Col: 40}, // field "stop" character,
{Row: 7, Col: 0, Content: "Change me . . ."},
{Row: 7, Col: 19, Name: "changeme", Content: "change me", Write: true, Highlighting: go3270.Underscore},
{Row: 7, Col: 40}, // field "stop" character
{Row: 8, Col: 0, Content: "Press"},
{Row: 8, Col: 6, Intense: true, Content: "enter"},
{Row: 8, Col: 12, Content: "to submit your name."},
{Row: 10, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"}, // a blank field for error messages
{Row: 22, Col: 0, Content: "PF3 Exit"},
}
var screen1rules = go3270.Rules{
"fname": {Validator: go3270.NonBlank},
"lname": {Validator: go3270.NonBlank},
"password": {Validator: go3270.NonBlank, Reset: true},
"changeme": {Validator: go3270.NonBlank, MustChange: true, ErrorText: "You can't leave \"change me\" as the value in the Change me field"},
}
var screen2 = go3270.Screen{
{Row: 0, Col: 27, Intense: true, Content: "3270 Example Application"},
{Row: 2, Col: 0, Content: "Thank you for submitting your name. Here's what I know:"},
{Row: 4, Col: 0, Content: "Your first name is"},
{Row: 4, Col: 19, Name: "fname"}, // We're giving this field a name to replace its value at runtime
{Row: 5, Col: 0, Content: "And your last name is"},
{Row: 5, Col: 22, Name: "lname"}, // We're giving this field a name to replace its value at runtime
{Row: 6, Col: 0, Name: "passwordOutput"},
{Row: 8, Col: 0, Content: "Press"},
{Row: 8, Col: 6, Intense: true, Content: "enter"},
{Row: 8, Col: 12, Content: "to enter your name again, or"},
{Row: 8, Col: 41, Intense: true, Content: "PF3"},
{Row: 8, Col: 45, Content: "to quit and disconnect."},
{Row: 11, Col: 0, Color: go3270.Turquoise, Highlighting: go3270.ReverseVideo, Content: "Here is a field with extended attributes."},
{Row: 11, Col: 42}, // remember to "stop" fields with a regular field, to clear the reverse video for example
{Row: 20, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"}, // a blank field for error messages
{Row: 22, Col: 0, Content: "PF3 Exit"},
}
func main() {
ln, err := net.Listen("tcp", ":3270")
if err != nil {
panic(err)
}
fmt.Println("LISTENING ON PORT 3270 FOR CONNECTIONS")
fmt.Println("Press Ctrl-C to end server.")
for {
conn, err := ln.Accept()
if err != nil {
panic(err)
}
go handle(conn)
}
}
// handle is the handler for individual user connections.
func handle(conn net.Conn) {
defer conn.Close()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
fieldValues := make(map[string]string)
// We will loop forever until the user quits
for {
// First we display screen 1. The HandleScreen() function handles
// input validation and processing attention keys we don't handle.
response, err := go3270.HandleScreen(
screen1, // the screen to display
screen1rules, // the rules to enforce
fieldValues, // any field values we wish to supply
[]go3270.AID{go3270.AIDEnter}, // the AID keys we support
[]go3270.AID{go3270.AIDPF3}, // keys that are "exit" keys
"errormsg", // the field to write error message into
4, 20, // the row and column to place the cursor
conn)
if err != nil {
fmt.Println(err)
return
}
if response.AID == go3270.AIDPF3 {
// Exit
break
}
fieldValues = response.Values
// Now we're ready to display screen2
passwordLength := len(strings.TrimSpace(fieldValues["password"]))
passwordPlural := "s"
if passwordLength == 1 {
passwordPlural = ""
}
fieldValues["passwordOutput"] = fmt.Sprintf("Your password was %d character%s long",
passwordLength, passwordPlural)
response, err = go3270.HandleScreen(
screen2, // the screen to display
nil, // the rules to enforce
fieldValues, // any field values we wish to supply
[]go3270.AID{go3270.AIDEnter}, // the AID keys we support
[]go3270.AID{go3270.AIDPF3}, // keys that are "exit" keys
"errormsg", // the field to write error message into
0, 0, // the row and column to place the cursor
conn)
if err != nil {
fmt.Println(err)
return
}
// If the user pressed PF3, exit
if response.AID == go3270.AIDPF3 {
break
}
// If they pressed anything else, just let the loop continue...
continue
}
fmt.Println("Connection closed")
}
+209
View File
@@ -0,0 +1,209 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
package go3270
import (
"fmt"
"net"
"regexp"
"strings"
)
// Rules is a map of field names (strings) to FieldRules structs. Each field
// for which you wish validation to occur must appear in the map. Fields not
// in the map will not have any input validation performed.
type Rules map[string]FieldRules
// Validator is a type that represents a function which can perform field
// input validation. The function is passed a string, input, and returns
// true if the input is valid or false if the not.
type Validator func(input string) bool
// NonBlank is a Validator that returns true if, after spaces are trimmed from
// the beginning and end of the string, the value is not empty.
var NonBlank Validator = func(input string) bool {
return !(strings.TrimSpace(input) == "")
}
var isIntegerRegexp = regexp.MustCompile(`^-?[0-9]+$`)
// IsInteger is a Validator that returns true if, after spaces are trimmed from
// the beginning and end if the string, the value is an integer (including
// negative numbers and 0).
var IsInteger Validator = func(input string) bool {
input = strings.TrimSpace(input)
return isIntegerRegexp.MatchString(input)
}
// FieldRules provides the validation rules for a particular field.
type FieldRules struct {
// MustChange, when true, indicates that the value of the field MUST be
// altered by the user -- if applied to a field with no starting value,
// this makes the field a required field. If true on a field with a
// starting value (either in the field's Content attribute, or with an
// override in the initial values map), then the user must change
// the value from the default.
MustChange bool
// ErrorText is the text displayed with the MustChange validation fails.
// If ErrorText is the empty string, but MustValidation fails, an error
// string will be constructed from the field name: "Please enter a valid
// value for <fieldName>."
ErrorText string
// Validator is a function to validate the value the user input into the
// field. It may be nil if no validation is required. The Validator
// function is called *after* the MustChange logic, so if you wish to
// fully handle validation, ensure MustChange is set to false.
Validator Validator
// Reset indicates that if the screen fails validation, this field should
// always be reset to its original/default value, regardless of what the
// user entered.
Reset bool
}
// HandleScreen is a higher-level interface to the ShowScreen() function.
// HandleScreen will loop until all validation rules are satisfied, and only
// return when an expected AID (i.e. PF) key is pressed.
//
// - screen is the Screen to display (see ShowScreen()).
// - rules are the Rules to enforce: each key in the Rules map corresponds to
// a Field.Name in the screen array.
// - values are field values you wish to override (see ShowScreen()).
// - pfkeys and exitkeys are the AID keys that you wish to accept (that is,
// perform validation and return if successful) and treat as exit keys
// (unconditionally return).
// - errorField is the name of a field in the screen array that you wish error
// messages to be written in when HandleScreen loops waiting for a valid
// user submission.
// - crow and ccol are the initial cursor position.
// - conn is the network connection to the 3270 client.
//
// HandleScreen will return when the user: 1) presses a key in pfkeys AND all
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
// other cases, HandleScreen will re-present the screen to the user again,
// possibly with an error message set in the errorField field.
func HandleScreen(screen Screen, rules Rules, values map[string]string,
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
conn net.Conn) (Response, error) {
// Save the original field values for any named fields to support
// the MustChange rule. Also build a map of named fields.
origValues := make(map[string]string)
fields := make(map[string]*Field)
for i := range screen {
if screen[i].Name != "" {
origValues[screen[i].Name] = screen[i].Content
fields[screen[i].Name] = &screen[i]
}
}
// Make our own field values map so we don't alter the caller's values
myValues := make(map[string]string)
for field := range values {
myValues[field] = values[field]
}
// Now we loop...
mainloop:
for {
// Reset fields with FieldRules.Reset set
for field := range rules {
if rules[field].Reset {
// avoid problems if there is a rule for a non-existent field
if _, ok := fields[field]; ok {
// Is the value in the origValues map?
if value, ok := origValues[field]; ok {
myValues[field] = value
} else {
// remove from the values map so we fall back to
// whatever default is set for the field
delete(myValues, field)
}
}
}
}
resp, err := ShowScreen(screen, myValues, crow, ccol, conn)
if err != nil {
return resp, err
}
// If we got an exit key, return without performing validation
if aidInArray(resp.AID, exitkeys) {
return resp, nil
}
// If we got an unexpected key, set error message and restart loop
if !aidInArray(resp.AID, pfkeys) {
if !(resp.AID == AIDClear || resp.AID == AIDPA1 || resp.AID == AIDPA2 ||
resp.AID == AIDPA3) {
myValues = mergeFieldValues(myValues, resp.Values)
}
myValues[errorField] = fmt.Sprintf("%s: unknown key",
AIDtoString(resp.AID))
continue
}
// At this point, we have an expected key. If one of the "clear" keys
// is expected, we can't do much, so we'll just return.
if resp.AID == AIDClear || resp.AID == AIDPA1 || resp.AID == AIDPA2 ||
resp.AID == AIDPA3 {
return resp, nil
}
myValues = mergeFieldValues(myValues, resp.Values)
delete(myValues, errorField) // don't persist errors across refreshes
// Now we can validate each field
for field := range rules {
// skip rules for fields that don't exist
if _, ok := myValues[field]; !ok {
continue
}
if rules[field].MustChange && myValues[field] == origValues[field] {
myValues[errorField] = rules[field].ErrorText
continue mainloop
}
if rules[field].Validator != nil && !rules[field].Validator(myValues[field]) {
myValues[errorField] = fmt.Sprintf("Value for %s is not valid", field)
continue mainloop
}
}
// Everything passed validation
return resp, nil
}
}
// aidInArray performs a linear search through the aids array and returns true
// if aid appears in the array, false otherwise.
func aidInArray(aid AID, aids []AID) bool {
for i := range aids {
if aids[i] == aid {
return true
}
}
return false
}
// mergeFieldValues will return a new map, containing all keys from the current
// map and keys from the original map that do not exist in the current map.
// This is sometimes necessary because the caller of HandleScreen() may
// provide override values for non-writable fields, and we don't get those
// values back when we round-trip with the 3270 client.
func mergeFieldValues(original, current map[string]string) map[string]string {
result := make(map[string]string)
for key := range current {
result[key] = current[key]
}
for key := range original {
if _, ok := result[key]; !ok {
result[key] = original[key]
}
}
return result
}
+68
View File
@@ -48,3 +48,71 @@ var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40,
41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1,
-1, -1, -1, -1}
// AIDtoString returns a string representation of an AID key name.
func AIDtoString(aid AID) string {
switch aid {
case AIDClear:
return "Clear"
case AIDEnter:
return "Enter"
case AIDNone:
return "[none]"
case AIDPA1:
return "PA1"
case AIDPA2:
return "PA2"
case AIDPA3:
return "PA3"
case AIDPF1:
return "PF1"
case AIDPF2:
return "PF2"
case AIDPF3:
return "PF3"
case AIDPF4:
return "PF4"
case AIDPF5:
return "PF5"
case AIDPF6:
return "PF6"
case AIDPF7:
return "PF7"
case AIDPF8:
return "PF8"
case AIDPF9:
return "PF9"
case AIDPF10:
return "PF10"
case AIDPF11:
return "PF11"
case AIDPF12:
return "PF12"
case AIDPF13:
return "PF13"
case AIDPF14:
return "PF14"
case AIDPF15:
return "PF15"
case AIDPF16:
return "PF16"
case AIDPF17:
return "PF17"
case AIDPF18:
return "PF18"
case AIDPF19:
return "PF19"
case AIDPF20:
return "PF20"
case AIDPF21:
return "PF21"
case AIDPF22:
return "PF22"
case AIDPF23:
return "PF23"
case AIDPF24:
return "PF24"
default:
return "[unknown]"
}
}