From fee74affa2642ed6cf2f8e3930ac17fb7116e6a9 Mon Sep 17 00:00:00 2001 From: "Matthew R. Wilson" Date: Mon, 7 Dec 2020 01:59:04 -0800 Subject: [PATCH] #1: Add HandleScreen() function as a higher-level interface to ShowScreen() --- .gitignore | 4 +- example/example.go => example1/example1.go | 0 example2/example2.go | 155 +++++++++++++++ looper.go | 209 +++++++++++++++++++++ util.go | 68 +++++++ 5 files changed, 434 insertions(+), 2 deletions(-) rename example/example.go => example1/example1.go (100%) create mode 100644 example2/example2.go create mode 100644 looper.go diff --git a/.gitignore b/.gitignore index 6cd7842..c156d32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ *.exe -example/example - +example1/example1 +example2/example2 diff --git a/example/example.go b/example1/example1.go similarity index 100% rename from example/example.go rename to example1/example1.go diff --git a/example2/example2.go b/example2/example2.go new file mode 100644 index 0000000..7f1df2b --- /dev/null +++ b/example2/example2.go @@ -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") +} diff --git a/looper.go b/looper.go new file mode 100644 index 0000000..485486b --- /dev/null +++ b/looper.go @@ -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 ." + 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 +} diff --git a/util.go b/util.go index c4713ca..1691b23 100644 --- a/util.go +++ b/util.go @@ -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]" + } +}