#1: Add HandleScreen() function as a higher-level interface to ShowScreen()
This commit is contained in:
1 parent
1e6ae603e7
commit
fee74affa2
5 files changed
+434
-2
No files matched your search
+2
-2
@@ -1,3 +1,3 @@
|
||||
*.exe
|
||||
example/example
|
||||
|
||||
example1/example1
|
||||
example2/example2
|
||||
File renamed without changes.
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user