Skip to content

Commit 9f8f3ab

Browse files
committed
Fix code review issues
1 parent 6d7c45f commit 9f8f3ab

File tree

12 files changed

+332
-349
lines changed

12 files changed

+332
-349
lines changed

docProps.go

Lines changed: 115 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"encoding/xml"
1717
"io"
1818
"reflect"
19-
"strconv"
19+
"slices"
2020
"time"
2121
)
2222

@@ -208,123 +208,6 @@ func (f *File) SetDocProps(docProperties *DocProperties) error {
208208
return err
209209
}
210210

211-
// SetDocCustomProps provides a function to set excel custom properties, support string, bool, float64,
212-
// time.DateTime four types.
213-
func (f *File) SetDocCustomProps(name string, value interface{}) error {
214-
customProps := new(xlsxCustomProperties)
215-
216-
// find existing custom properties
217-
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
218-
Decode(customProps); err != nil && err != io.EOF {
219-
return err
220-
}
221-
222-
props := customProps.Props
223-
existingPropertyMap := make(map[string]xlsxCustomProperty)
224-
maxPID := 1 // pid from 2
225-
for _, prop := range props {
226-
pid, err := strconv.Atoi(prop.PID)
227-
if err == nil && pid > maxPID {
228-
maxPID = pid
229-
}
230-
231-
existingPropertyMap[prop.Name] = prop
232-
}
233-
234-
// different custom property value type setter function
235-
var setValueFunc func(*xlsxCustomProperty) error
236-
switch v := value.(type) {
237-
case float64:
238-
setValueFunc = func(property *xlsxCustomProperty) error {
239-
property.Number = &NumberValue{Number: v}
240-
return nil
241-
}
242-
case bool:
243-
setValueFunc = func(property *xlsxCustomProperty) error {
244-
property.Bool = &BoolValue{Bool: v}
245-
return nil
246-
}
247-
case string:
248-
setValueFunc = func(property *xlsxCustomProperty) error {
249-
property.Text = &TextValue{Text: v}
250-
return nil
251-
}
252-
case time.Time:
253-
setValueFunc = func(property *xlsxCustomProperty) error {
254-
property.DateTime = &FileTimeValue{
255-
DateTime: v.Format(time.RFC3339),
256-
}
257-
return nil
258-
}
259-
default:
260-
setValueFunc = func(_ *xlsxCustomProperty) error {
261-
return ErrUnsupportedCustomPropertyDataType
262-
}
263-
}
264-
265-
// update existing custom properties
266-
if existingProperty, ok := existingPropertyMap[name]; ok {
267-
if err := setValueFunc(&existingProperty); err != nil {
268-
return err
269-
}
270-
} else {
271-
// add new custom property
272-
newProperty := xlsxCustomProperty{
273-
FmtID: CustomPropertiesFMTID,
274-
PID: strconv.FormatInt(int64(maxPID+1), 10), // max pid plus 1 to create a new unique pid
275-
Name: name,
276-
}
277-
278-
if err := setValueFunc(&newProperty); err != nil {
279-
return err
280-
}
281-
282-
props = append(props, newProperty)
283-
}
284-
285-
newCustomProps := &xlsxCustomProperties{
286-
Vt: NameSpaceDocumentPropertiesVariantTypes.Value,
287-
Props: props,
288-
}
289-
290-
output, err := xml.Marshal(newCustomProps)
291-
f.saveFileList(defaultXMLPathDocPropsCustom, output)
292-
293-
// set custom properties if necessary
294-
_ = f.addRels(defaultXMLRels, SourceRelationshipCustomProperties, defaultXMLPathDocPropsCustom, "")
295-
296-
// set content type if necessary
297-
_ = f.setContentTypes("/"+defaultXMLPathDocPropsCustom, ContentTypeCustomProperties)
298-
299-
return err
300-
}
301-
302-
// GetDocCustomProps provides a function to get document custom properties, supported string, bool, float64,
303-
// time.DateTime four types. If the custom property is not set, it will return an empty map, and if the custom property
304-
// value is invalid format, it returns as if the custom property does not exist.
305-
func (f *File) GetDocCustomProps() (kv map[string]interface{}, err error) {
306-
custom := new(xlsxCustomProperties)
307-
308-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
309-
Decode(custom); err != nil && err != io.EOF {
310-
return
311-
}
312-
313-
kv = make(map[string]interface{})
314-
if custom == nil || len(custom.Props) == 0 {
315-
return kv, nil
316-
}
317-
318-
for _, prop := range custom.Props {
319-
propertyValue := prop.getPropertyValue()
320-
if propertyValue != nil {
321-
kv[prop.Name] = propertyValue
322-
}
323-
}
324-
325-
return kv, nil
326-
}
327-
328211
// GetDocProps provides a function to get document core properties.
329212
func (f *File) GetDocProps() (ret *DocProperties, err error) {
330213
core := new(decodeCoreProperties)
@@ -355,3 +238,117 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) {
355238
}
356239
return
357240
}
241+
242+
// SetCustomProps provides a function to set custom file properties by given
243+
// property name and value. If the property name already exists, it will be
244+
// updated, otherwise a new property will be added. The value can be of type
245+
// int32, float64, bool, string, time.Time or nil. The property will be delete
246+
// if the value is nil. The function returns an error if the property value is
247+
// not of the correct type.
248+
func (f *File) SetCustomProps(prop CustomProperty) error {
249+
if prop.Name == "" {
250+
return ErrParameterInvalid
251+
}
252+
props := new(decodeCustomProperties)
253+
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
254+
Decode(props); err != nil && err != io.EOF {
255+
return err
256+
}
257+
customProps := xlsxCustomProperties{Vt: NameSpaceDocumentPropertiesVariantTypes.Value}
258+
idx, pID := -1, 1
259+
for i := range props.Property {
260+
p := new(xlsxProperty)
261+
setPtrFields(reflect.ValueOf(&props.Property[i]).Elem(), reflect.ValueOf(p).Elem())
262+
if pID < props.Property[i].PID {
263+
pID = props.Property[i].PID
264+
}
265+
if props.Property[i].Name == prop.Name {
266+
idx = i
267+
}
268+
customProps.Property = append(customProps.Property, *p)
269+
}
270+
if idx != -1 && prop.Value == nil {
271+
customProps.Property = slices.Delete(customProps.Property, idx, idx+1)
272+
}
273+
if prop.Value != nil {
274+
property := xlsxProperty{Name: prop.Name, FmtID: EXtURICustomPropertyFmtID}
275+
if err := property.setCustomProps(prop.Value); err != nil {
276+
return err
277+
}
278+
if idx != -1 {
279+
property.PID = customProps.Property[idx].PID
280+
customProps.Property[idx] = property
281+
} else {
282+
property.PID = pID + 1
283+
customProps.Property = append(customProps.Property, property)
284+
}
285+
}
286+
_ = f.addRels(defaultXMLPathRels, SourceRelationshipCustomProperties, defaultXMLPathDocPropsCustom, "")
287+
if err := f.addContentTypePart(0, "customProperties"); err != nil {
288+
return err
289+
}
290+
output, err := xml.Marshal(customProps)
291+
f.saveFileList(defaultXMLPathDocPropsCustom, output)
292+
return err
293+
}
294+
295+
// setCustomProps sets the custom property value based on its type.
296+
func (prop *xlsxProperty) setCustomProps(value interface{}) error {
297+
switch v := value.(type) {
298+
case int32:
299+
prop.I4 = &v
300+
case float64:
301+
prop.R8 = float64Ptr(v)
302+
case bool:
303+
prop.Bool = boolPtr(v)
304+
case string:
305+
prop.Lpwstr = stringPtr(value.(string))
306+
case time.Time:
307+
prop.FileTime = stringPtr(value.(time.Time).Format(time.RFC3339))
308+
default:
309+
return ErrParameterInvalid
310+
}
311+
return nil
312+
}
313+
314+
// GetCustomProps provides a function to get custom file properties.
315+
func (f *File) GetCustomProps() ([]CustomProperty, error) {
316+
var customProps []CustomProperty
317+
props := new(decodeCustomProperties)
318+
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
319+
Decode(props); err != nil && err != io.EOF {
320+
return customProps, err
321+
}
322+
for _, p := range props.Property {
323+
prop := CustomProperty{Name: p.Name}
324+
var err error
325+
if prop.Value, err = p.getCustomProps(); err != nil {
326+
return customProps, err
327+
}
328+
customProps = append(customProps, prop)
329+
}
330+
return customProps, nil
331+
}
332+
333+
// getCustomProps gets the custom property value based on its type.
334+
func (p *decodeProperty) getCustomProps() (interface{}, error) {
335+
s := reflect.ValueOf(p).Elem()
336+
for i := range s.NumField() {
337+
if 11 <= i && i <= 20 && !s.Field(i).IsNil() {
338+
return int32(s.Field(i).Elem().Int()), nil // Field vt:i1 to vt:uint
339+
}
340+
if 21 <= i && i <= 22 && !s.Field(i).IsNil() {
341+
return s.Field(i).Elem().Float(), nil // Field vt:r4 to vt:r8
342+
}
343+
if p.Bool != nil {
344+
return *p.Bool, nil
345+
}
346+
if p.Lpwstr != nil {
347+
return *p.Lpwstr, nil
348+
}
349+
if p.FileTime != nil {
350+
return time.Parse(time.RFC3339, *p.FileTime)
351+
}
352+
}
353+
return nil, nil
354+
}

docProps_test.go

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
package excelize
1313

1414
import (
15+
"fmt"
1516
"path/filepath"
17+
"slices"
1618
"testing"
1719
"time"
1820

@@ -98,30 +100,6 @@ func TestSetDocProps(t *testing.T) {
98100
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "XML syntax error on line 1: invalid UTF-8")
99101
}
100102

101-
func TestSetDocCustomProps(t *testing.T) {
102-
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
103-
if !assert.NoError(t, err) {
104-
t.FailNow()
105-
}
106-
107-
assert.NoError(t, f.SetDocCustomProps("string", "v1.0.0"))
108-
assert.NoError(t, f.SetDocCustomProps("string", "v2.0.0"))
109-
assert.EqualError(t, f.SetDocCustomProps("string", int64(1)), ErrUnsupportedCustomPropertyDataType.Error())
110-
assert.NoError(t, f.SetDocCustomProps("bool", true))
111-
assert.EqualError(t, f.SetDocCustomProps("int64", int64(1)), ErrUnsupportedCustomPropertyDataType.Error())
112-
assert.NoError(t, f.SetDocCustomProps("float64", 1.0))
113-
assert.NoError(t, f.SetDocCustomProps("date", time.Now()))
114-
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocCustomProps.xlsx")))
115-
f.Pkg.Store(defaultXMLPathDocPropsCustom, nil)
116-
assert.NoError(t, f.SetDocCustomProps("version", ""))
117-
assert.NoError(t, f.Close())
118-
119-
// Test unsupported charset
120-
f = NewFile()
121-
f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
122-
assert.EqualError(t, f.SetDocCustomProps("version", ""), "XML syntax error on line 1: invalid UTF-8")
123-
}
124-
125103
func TestGetDocProps(t *testing.T) {
126104
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
127105
if !assert.NoError(t, err) {
@@ -142,33 +120,69 @@ func TestGetDocProps(t *testing.T) {
142120
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
143121
}
144122

145-
func TestFile_GetDocCustomProps(t *testing.T) {
146-
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
147-
if !assert.NoError(t, err) {
148-
t.FailNow()
123+
func TestCustomProps(t *testing.T) {
124+
f := NewFile()
125+
expected := []CustomProperty{
126+
{Name: "Text Prop", Value: "text"},
127+
{Name: "Boolean Prop 1", Value: true},
128+
{Name: "Boolean Prop 2", Value: false},
129+
{Name: "Number Prop 1", Value: -123.456},
130+
{Name: "Number Prop 2", Value: int32(1)},
131+
{Name: "Date Prop", Value: time.Date(2021, time.September, 11, 0, 0, 0, 0, time.UTC)},
149132
}
150-
151-
// now no custom properties in f
152-
props, err := f.GetDocCustomProps()
133+
for _, prop := range expected {
134+
assert.NoError(t, f.SetCustomProps(prop))
135+
}
136+
props, err := f.GetCustomProps()
153137
assert.NoError(t, err)
154-
assert.Empty(t, props)
138+
assert.Equal(t, expected, props)
155139

156-
assert.NoError(t, f.SetDocCustomProps("string", "v1.0.0"))
157-
assert.NoError(t, f.SetDocCustomProps("bool", true))
158-
assert.NoError(t, f.SetDocCustomProps("float64", 1.0))
159-
dateValue := time.Date(2006, 01, 02, 15, 04, 05, 0, time.Local)
160-
assert.NoError(t, f.SetDocCustomProps("date", dateValue))
140+
// Test delete custom property
141+
assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 1", Value: nil}))
142+
props, err = f.GetCustomProps()
143+
assert.NoError(t, err)
144+
expected = slices.Delete(expected, 1, 2)
145+
assert.Equal(t, expected, props)
161146

162-
props, err = f.GetDocCustomProps()
147+
// Test change custom property value data type
148+
assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: "true"}))
149+
props, err = f.GetCustomProps()
163150
assert.NoError(t, err)
164-
assert.Equal(t, "v1.0.0", props["string"])
165-
assert.Equal(t, true, props["bool"])
166-
assert.Equal(t, 1.0, props["float64"])
167-
assert.Equal(t, dateValue.Unix(), props["date"].(time.Time).Unix())
151+
assert.Equal(t, props[1].Value, "true")
168152

169-
// Test get workbook properties with unsupported charset
153+
// Test set custom property with unsupported value data type
154+
assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: 1}))
155+
156+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCustomProps.xlsx")))
157+
assert.NoError(t, f.Close())
158+
159+
// Test set custom property without property name
170160
f = NewFile()
161+
assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{}))
162+
163+
// Test set custom property with unsupported charset
171164
f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
172-
_, err = f.GetDocCustomProps()
165+
assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
166+
167+
// Test get custom property with unsupported charset
168+
_, err = f.GetCustomProps()
173169
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
170+
171+
// Test set custom property with unsupported charset content types
172+
f = NewFile()
173+
f.ContentTypes = nil
174+
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
175+
assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
176+
177+
// Test get custom property with unsupported charset
178+
f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="%s"><property fmtid="%s" pid="2" name="Prop"><vt:filetime>x</vt:filetime></property></Properties>`, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
179+
_, err = f.GetCustomProps()
180+
assert.EqualError(t, err, "parsing time \"x\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"x\" as \"2006\"")
181+
182+
// Test get custom property with unsupported value data type
183+
f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="%s"><property fmtid="%s" pid="2" name="Prop"><vt:cy></vt:cy></property></Properties>`, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
184+
props, err = f.GetCustomProps()
185+
assert.Equal(t, []CustomProperty{{Name: "Prop"}}, props)
186+
assert.NoError(t, err)
187+
assert.NoError(t, f.Close())
174188
}

errors.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,6 @@ var (
178178
// ErrWorkbookPassword defined the error message on receiving the incorrect
179179
// workbook password.
180180
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
181-
// ErrUnsupportedCustomPropertyDataType defined the error message on unsupported custom property value data type.
182-
ErrUnsupportedCustomPropertyDataType = errors.New("unsupported custom property value data type")
183181
)
184182

185183
// ErrSheetNotExist defined an error of sheet that does not exist.

0 commit comments

Comments
 (0)