// Copyright 2020 The Xorm Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package tags import ( "encoding/gob" "errors" "fmt" "reflect" "strings" "sync" "time" "xorm.io/xorm/caches" "xorm.io/xorm/convert" "xorm.io/xorm/dialects" "xorm.io/xorm/names" "xorm.io/xorm/schemas" ) var ( ErrUnsupportedType = errors.New("Unsupported type") ) type Parser struct { identifier string dialect dialects.Dialect columnMapper names.Mapper tableMapper names.Mapper handlers map[string]Handler cacherMgr *caches.Manager tableCache sync.Map // map[reflect.Type]*schemas.Table } func NewParser(identifier string, dialect dialects.Dialect, tableMapper, columnMapper names.Mapper, cacherMgr *caches.Manager) *Parser { return &Parser{ identifier: identifier, dialect: dialect, tableMapper: tableMapper, columnMapper: columnMapper, handlers: defaultTagHandlers, cacherMgr: cacherMgr, } } func (parser *Parser) GetTableMapper() names.Mapper { return parser.tableMapper } func (parser *Parser) SetTableMapper(mapper names.Mapper) { parser.ClearCaches() parser.tableMapper = mapper } func (parser *Parser) GetColumnMapper() names.Mapper { return parser.columnMapper } func (parser *Parser) SetColumnMapper(mapper names.Mapper) { parser.ClearCaches() parser.columnMapper = mapper } func (parser *Parser) ParseWithCache(v reflect.Value) (*schemas.Table, error) { t := v.Type() tableI, ok := parser.tableCache.Load(t) if ok { return tableI.(*schemas.Table), nil } table, err := parser.Parse(v) if err != nil { return nil, err } parser.tableCache.Store(t, table) if parser.cacherMgr.GetDefaultCacher() != nil { if v.CanAddr() { gob.Register(v.Addr().Interface()) } else { gob.Register(v.Interface()) } } return table, nil } // ClearCacheTable removes the database mapper of a type from the cache func (parser *Parser) ClearCacheTable(t reflect.Type) { parser.tableCache.Delete(t) } // ClearCaches removes all the cached table information parsed by structs func (parser *Parser) ClearCaches() { parser.tableCache = sync.Map{} } func addIndex(indexName string, table *schemas.Table, col *schemas.Column, indexType int) { if index, ok := table.Indexes[indexName]; ok { index.AddColumn(col.Name) col.Indexes[index.Name] = indexType } else { index := schemas.NewIndex(indexName, indexType) index.AddColumn(col.Name) table.AddIndex(index) col.Indexes[index.Name] = indexType } } // Parse parses a struct as a table information func (parser *Parser) Parse(v reflect.Value) (*schemas.Table, error) { t := v.Type() if t.Kind() == reflect.Ptr { t = t.Elem() v = v.Elem() } if t.Kind() != reflect.Struct { return nil, ErrUnsupportedType } table := schemas.NewEmptyTable() table.Type = t table.Name = names.GetTableName(parser.tableMapper, v) var idFieldColName string var hasCacheTag, hasNoCacheTag bool for i := 0; i < t.NumField(); i++ { tag := t.Field(i).Tag ormTagStr := tag.Get(parser.identifier) var col *schemas.Column fieldValue := v.Field(i) fieldType := fieldValue.Type() if ormTagStr != "" { col = &schemas.Column{ FieldName: t.Field(i).Name, Nullable: true, IsPrimaryKey: false, IsAutoIncrement: false, MapType: schemas.TWOSIDES, Indexes: make(map[string]int), DefaultIsEmpty: true, } tags := splitTag(ormTagStr) if len(tags) > 0 { if tags[0] == "-" { continue } var ctx = Context{ table: table, col: col, fieldValue: fieldValue, indexNames: make(map[string]int), parser: parser, } if strings.HasPrefix(strings.ToUpper(tags[0]), "EXTENDS") { pStart := strings.Index(tags[0], "(") if pStart > -1 && strings.HasSuffix(tags[0], ")") { var tagPrefix = strings.TrimFunc(tags[0][pStart+1:len(tags[0])-1], func(r rune) bool { return r == '\'' || r == '"' }) ctx.params = []string{tagPrefix} } if err := ExtendsTagHandler(&ctx); err != nil { return nil, err } continue } for j, key := range tags { if ctx.ignoreNext { ctx.ignoreNext = false continue } k := strings.ToUpper(key) ctx.tagName = k ctx.params = []string{} pStart := strings.Index(k, "(") if pStart == 0 { return nil, errors.New("( could not be the first character") } if pStart > -1 { if !strings.HasSuffix(k, ")") { return nil, fmt.Errorf("field %s tag %s cannot match ) character", col.FieldName, key) } ctx.tagName = k[:pStart] ctx.params = strings.Split(key[pStart+1:len(k)-1], ",") } if j > 0 { ctx.preTag = strings.ToUpper(tags[j-1]) } if j < len(tags)-1 { ctx.nextTag = tags[j+1] } else { ctx.nextTag = "" } if h, ok := parser.handlers[ctx.tagName]; ok { if err := h(&ctx); err != nil { return nil, err } } else { if strings.HasPrefix(key, "'") && strings.HasSuffix(key, "'") { col.Name = key[1 : len(key)-1] } else { col.Name = key } } if ctx.hasCacheTag { hasCacheTag = true } if ctx.hasNoCacheTag { hasNoCacheTag = true } } if col.SQLType.Name == "" { col.SQLType = schemas.Type2SQLType(fieldType) } parser.dialect.SQLType(col) if col.Length == 0 { col.Length = col.SQLType.DefaultLength } if col.Length2 == 0 { col.Length2 = col.SQLType.DefaultLength2 } if col.Name == "" { col.Name = parser.columnMapper.Obj2Table(t.Field(i).Name) } if ctx.isUnique { ctx.indexNames[col.Name] = schemas.UniqueType } else if ctx.isIndex { ctx.indexNames[col.Name] = schemas.IndexType } for indexName, indexType := range ctx.indexNames { addIndex(indexName, table, col, indexType) } } } else { var sqlType schemas.SQLType if fieldValue.CanAddr() { if _, ok := fieldValue.Addr().Interface().(convert.Conversion); ok { sqlType = schemas.SQLType{Name: schemas.Text} } } if _, ok := fieldValue.Interface().(convert.Conversion); ok { sqlType = schemas.SQLType{Name: schemas.Text} } else { sqlType = schemas.Type2SQLType(fieldType) } col = schemas.NewColumn(parser.columnMapper.Obj2Table(t.Field(i).Name), t.Field(i).Name, sqlType, sqlType.DefaultLength, sqlType.DefaultLength2, true) if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { idFieldColName = col.Name } } if col.IsAutoIncrement { col.Nullable = false } table.AddColumn(col) } // end for if idFieldColName != "" && len(table.PrimaryKeys) == 0 { col := table.GetColumn(idFieldColName) col.IsPrimaryKey = true col.IsAutoIncrement = true col.Nullable = false table.PrimaryKeys = append(table.PrimaryKeys, col.Name) table.AutoIncrement = col.Name } if hasCacheTag { if parser.cacherMgr.GetDefaultCacher() != nil { // !nash! use engine's cacher if provided //engine.logger.Info("enable cache on table:", table.Name) parser.cacherMgr.SetCacher(table.Name, parser.cacherMgr.GetDefaultCacher()) } else { //engine.logger.Info("enable LRU cache on table:", table.Name) parser.cacherMgr.SetCacher(table.Name, caches.NewLRUCacher2(caches.NewMemoryStore(), time.Hour, 10000)) } } if hasNoCacheTag { //engine.logger.Info("disable cache on table:", table.Name) parser.cacherMgr.SetCacher(table.Name, nil) } return table, nil }