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 }