github.com/boki/go-xmp@v1.0.1/xmp/sync.go (about)

     1  // Copyright (c) 2017-2018 Alexander Eichhorn
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License"): you may
     4  // not use this file except in compliance with the License. You may obtain
     5  // a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    11  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    12  // License for the specific language governing permissions and limitations
    13  // under the License.
    14  
    15  // Sync across different models by specifying paths to values within each
    16  // model. Paths must begin with a registered namespace short name followed
    17  // by a colon (:). Paths must be the same strings as used in type's XMP tag,
    18  // e.g
    19  //
    20  //     XMP Tag                      XMP Path
    21  //     `xmp:"xmpDM:cameraAngle"`    xmpDM:cameraAngle
    22  //
    23  // To refer nested structs or arrays, paths may contain multiple segments
    24  // separated by a forward slash (/). Array elements may be addressed using
    25  // square brackets after the XMP name of the array, e.g.
    26  //
    27  //     XMP Array
    28  //     dc:creator[0]       // the first array element in XMP lists and bags
    29  //     dc:description[en]  // a language entry in XMP alternative arrays
    30  //
    31  // Sync flags
    32  //
    33  // Syncing can be controlled by flags. Flags are binary and can be combined to
    34  // achieve a desired result. Some useful combinations are
    35  //
    36  //   S_CREATE|S_REPLACE|S_DELETE
    37  // 	   create dest when not exist, overwrite when exist, clear when exist and
    38  //     source is empty
    39  //
    40  //   S_CREATE|S_DELETE
    41  // 	   create dest when not exist, clear when exist and source is empty
    42  //
    43  //   S_CREATE|S_REPLACE
    44  //     create when not exist, overwrite when exist unless source is empty
    45  //
    46  // You may find other combinations helpful as well. When working with lists
    47  // as sync destination, combine the above with S_APPEND or S_UNIQUE to extend
    48  // a list. When S_DELETE is set and the source value is empty, the list will
    49  // be cleared. With S_REPLACE, a list will be replaced by the source value,
    50  // that is, afterwards the list contains a single entry only. Precedence order
    51  // for slices/arrays when multiple flags are set is UNIQUE > APPEND > REPLACE.
    52  //
    53  // Examples
    54  //   "xmpDM:cameraAngle" <-> "trim:Shot/Angle"
    55  //   "tiff:Artist" <-> "dc:creator"
    56  //
    57  //
    58  
    59  package xmp
    60  
    61  import (
    62  	"fmt"
    63  	"strings"
    64  )
    65  
    66  type SyncDesc struct {
    67  	Source  Path
    68  	Dest    Path
    69  	Flags   SyncFlags
    70  	Convert ConverterFunc
    71  }
    72  
    73  type SyncDescList []*SyncDesc
    74  
    75  type ConverterFunc func(string) string
    76  
    77  type SyncFlags int
    78  
    79  const (
    80  	CREATE  SyncFlags = 1 << iota // create when not exist, nothing when exist or source is empty
    81  	REPLACE                       // replace when dest exist, nothing when missing or source is empty
    82  	DELETE                        // clear dest when source value is empty
    83  	APPEND                        // list-only: append non-empty source value
    84  	UNIQUE                        // list-only: append non-empty unique source value
    85  	NOFAIL                        // don't fail when state+op+flags don't match
    86  	DEFAULT = CREATE | REPLACE | DELETE | UNIQUE
    87  	MERGE   = CREATE | UNIQUE | NOFAIL
    88  	EXTEND  = CREATE | REPLACE | UNIQUE | NOFAIL
    89  	ADD     = CREATE | UNIQUE | NOFAIL
    90  )
    91  
    92  func ParseSyncFlag(s string) SyncFlags {
    93  	switch s {
    94  	case "create":
    95  		return CREATE
    96  	case "replace":
    97  		return REPLACE
    98  	case "delete":
    99  		return DELETE
   100  	case "append":
   101  		return APPEND
   102  	case "unique":
   103  		return UNIQUE
   104  	case "nofail":
   105  		return NOFAIL
   106  	case "default":
   107  		return DEFAULT
   108  	case "merge":
   109  		return MERGE
   110  	case "extend":
   111  		return EXTEND
   112  	case "add":
   113  		return ADD
   114  	default:
   115  		return 0
   116  	}
   117  }
   118  
   119  func ParseSyncFlags(s string) (SyncFlags, error) {
   120  	var flags SyncFlags
   121  	for _, v := range strings.Split(s, ",") {
   122  		f := ParseSyncFlag(strings.ToLower(v))
   123  		if f == 0 {
   124  			return 0, fmt.Errorf("invalid xmp sync flag '%s'", v)
   125  		}
   126  		flags |= f
   127  	}
   128  	return flags, nil
   129  }
   130  
   131  func (f *SyncFlags) UnmarshalText(data []byte) error {
   132  	if len(data) == 0 {
   133  		*f = 0
   134  		return nil
   135  	}
   136  	if flags, err := ParseSyncFlags(string(data)); err != nil {
   137  		return err
   138  	} else {
   139  		*f = flags
   140  	}
   141  	return nil
   142  }
   143  
   144  // Model is optional and exists for performance reasons to avoid lookups when
   145  // a sync destination is a specific model only (useful to implement SyncFromXMP()
   146  // in models)
   147  func (d *Document) SyncMulti(desc SyncDescList, m Model) error {
   148  	for _, v := range desc {
   149  		if err := d.Sync(v.Source, v.Dest, v.Flags, m, v.Convert); err != nil {
   150  			return err
   151  		}
   152  	}
   153  	return nil
   154  }
   155  
   156  func (d *Document) Sync(sPath, dPath Path, flags SyncFlags, v Model, f ConverterFunc) error {
   157  	// use default flags when zero
   158  	if flags == 0 {
   159  		flags = DEFAULT
   160  	}
   161  
   162  	// only XMP paths are supported here
   163  	if !sPath.IsXmpPath() || !dPath.IsXmpPath() {
   164  		return nil
   165  	}
   166  
   167  	sNs, _ := sPath.Namespace(d)
   168  	dNs, _ := dPath.Namespace(d)
   169  
   170  	// skip when either namespace does not exist
   171  	if sNs == nil || dNs == nil {
   172  		return nil
   173  	}
   174  
   175  	// skip when sPath model does not exist
   176  	sModel := d.FindModel(sNs)
   177  	if sModel == nil {
   178  		return nil
   179  	}
   180  
   181  	dModel := v
   182  	if dModel != nil {
   183  		// dPath model must match dPath namespace
   184  		if !dPath.MatchNamespace(dModel.Namespaces()[0]) {
   185  			return nil
   186  		}
   187  	} else {
   188  		dModel = d.FindModel(dNs)
   189  	}
   190  
   191  	// create dPath model
   192  	if dModel == nil {
   193  		if flags&CREATE == 0 {
   194  			return nil
   195  		}
   196  		dModel = dNs.NewModel()
   197  		d.AddModel(dModel)
   198  	}
   199  
   200  	sValue, err := GetModelPath(sModel, sPath)
   201  	if err != nil {
   202  		if flags&NOFAIL > 0 {
   203  			return nil
   204  		}
   205  		return err
   206  	}
   207  	dValue, err := GetModelPath(dModel, dPath)
   208  	if err != nil {
   209  		if flags&NOFAIL > 0 {
   210  			return nil
   211  		}
   212  		return err
   213  	}
   214  
   215  	// skip when equal
   216  	if sValue == dValue {
   217  		return nil
   218  	}
   219  
   220  	// empty source will only be used with delete flag
   221  	if sValue == "" && flags&DELETE == 0 {
   222  		return nil
   223  	}
   224  
   225  	// empty destination values require create flag
   226  	if dValue == "" && flags&CREATE == 0 {
   227  		return nil
   228  	}
   229  
   230  	// existing destination values require replace/delete/append/unique flag
   231  	if dValue != "" && flags&(REPLACE|DELETE|APPEND|UNIQUE) == 0 {
   232  		return nil
   233  	}
   234  
   235  	// convert the source value if requested
   236  	if f != nil {
   237  		sValue = f(sValue)
   238  	}
   239  
   240  	if err = SetModelPath(dModel, dPath, sValue, flags); err != nil {
   241  		if flags&NOFAIL > 0 {
   242  			return nil
   243  		}
   244  		return err
   245  	}
   246  	d.SetDirty()
   247  	return nil
   248  }
   249  
   250  func (d *Document) Merge(b *Document, flags SyncFlags) error {
   251  	p, err := b.ListPaths()
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	// copy namespaces
   257  	for n, v := range b.intNsMap {
   258  		d.intNsMap[n] = v
   259  	}
   260  	for n, v := range b.extNsMap {
   261  		d.extNsMap[n] = v
   262  	}
   263  
   264  	// copy content
   265  	for _, v := range p {
   266  		v.Flags = flags
   267  		if err := d.SetPath(v); err != nil {
   268  			return err
   269  		}
   270  	}
   271  
   272  	d.SetDirty()
   273  	return nil
   274  }