Support for ResXResourceWriter.BasePath being deeper than actual filepath
[mono.git] / mcs / class / Managed.Windows.Forms / System.Resources / ResXResourceWriter.cs
1 // Permission is hereby granted, free of charge, to any person obtaining
2 // a copy of this software and associated documentation files (the
3 // "Software"), to deal in the Software without restriction, including
4 // without limitation the rights to use, copy, modify, merge, publish,
5 // distribute, sublicense, and/or sell copies of the Software, and to
6 // permit persons to whom the Software is furnished to do so, subject to
7 // the following conditions:
8 // 
9 // The above copyright notice and this permission notice shall be
10 // included in all copies or substantial portions of the Software.
11 // 
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 //
20 // Copyright (c) 2004-2005 Novell, Inc.
21 //
22 // Authors:
23 //      Duncan Mak              duncan@ximian.com
24 //      Gonzalo Paniagua Javier gonzalo@ximian.com
25 //      Peter Bartok            pbartok@novell.com
26 //      Gary Barnett
27 //      includes code by Mike Krüger and Lluis Sanchez
28
29 using System.ComponentModel;
30 using System.IO;
31 using System.Runtime.Serialization.Formatters.Binary;
32 using System.Text;
33 using System.Xml;
34 using System.Reflection;
35
36 namespace System.Resources
37 {
38 #if INSIDE_SYSTEM_WEB
39         internal
40 #else
41         public
42 #endif
43         class ResXResourceWriter : IResourceWriter, IDisposable
44         {
45                 #region Local Variables
46                 private string          filename;
47                 private Stream          stream;
48                 private TextWriter      textwriter;
49                 private XmlTextWriter   writer;
50                 private bool            written;
51                 private string          base_path;
52                 #endregion      // Local Variables
53
54                 #region Static Fields
55                 public static readonly string BinSerializedObjectMimeType       = "application/x-microsoft.net.object.binary.base64";
56                 public static readonly string ByteArraySerializedObjectMimeType = "application/x-microsoft.net.object.bytearray.base64";
57                 public static readonly string DefaultSerializedObjectMimeType   = BinSerializedObjectMimeType;
58                 public static readonly string ResMimeType                       = "text/microsoft-resx";
59                 public static readonly string ResourceSchema                    = schema;
60                 public static readonly string SoapSerializedObjectMimeType      = "application/x-microsoft.net.object.soap.base64";
61                 public static readonly string Version                           = "2.0";
62                 #endregion      // Static Fields
63
64                 #region Constructors & Destructor
65                 public ResXResourceWriter (Stream stream)
66                 {
67                         if (stream == null)
68                                 throw new ArgumentNullException ("stream");
69
70                         if (!stream.CanWrite)
71                                 throw new ArgumentException ("stream is not writable.", "stream");
72
73                         this.stream = stream;
74                 }
75
76                 public ResXResourceWriter (TextWriter textWriter)
77                 {
78                         if (textWriter == null)
79                                 throw new ArgumentNullException ("textWriter");
80
81                         this.textwriter = textWriter;
82                 }
83                 
84                 public ResXResourceWriter (string fileName)
85                 {
86                         if (fileName == null)
87                                 throw new ArgumentNullException ("fileName");
88
89                         this.filename = fileName;
90                 }
91
92                 ~ResXResourceWriter() {
93                         Dispose(false);
94                 }
95                 #endregion      // Constructors & Destructor
96
97                 void InitWriter ()
98                 {
99                         if (filename != null)
100                                 stream = File.Open (filename, FileMode.Create);
101                         if (textwriter == null)
102                                 textwriter = new StreamWriter (stream, Encoding.UTF8);
103
104                         writer = new XmlTextWriter (textwriter);
105                         writer.Formatting = Formatting.Indented;
106                         writer.WriteStartDocument ();
107                         writer.WriteStartElement ("root");
108                         writer.WriteRaw (schema);
109                         WriteHeader ("resmimetype", "text/microsoft-resx");
110                         WriteHeader ("version", "1.3");
111                         WriteHeader ("reader", typeof (ResXResourceReader).AssemblyQualifiedName);
112                         WriteHeader ("writer", typeof (ResXResourceWriter).AssemblyQualifiedName);
113                 }
114
115                 void WriteHeader (string name, string value)
116                 {
117                         writer.WriteStartElement ("resheader");
118                         writer.WriteAttributeString ("name", name);
119                         writer.WriteStartElement ("value");
120                         writer.WriteString (value);
121                         writer.WriteEndElement ();
122                         writer.WriteEndElement ();
123                 }
124
125                 void WriteNiceBase64(byte[] value, int offset, int length) {
126                         string          b64;
127                         StringBuilder   sb;
128                         int             pos;
129                         int             inc;
130                         string          ins;
131
132                         b64 = Convert.ToBase64String(value, offset, length);
133
134                         // Wild guess; two extra newlines, and one newline/tab pair for every 80 chars
135                         sb = new StringBuilder(b64, b64.Length + ((b64.Length + 160) / 80) * 3);
136                         pos = 0;
137                         inc = 80 + Environment.NewLine.Length + 1;
138                         ins = Environment.NewLine + "\t";
139                         while (pos < sb.Length) {
140                                 sb.Insert(pos, ins);
141                                 pos += inc;
142                         }
143                         sb.Insert(sb.Length, Environment.NewLine);
144                         writer.WriteString(sb.ToString());
145                 }
146                 void WriteBytes (string name, Type type, byte[] value, int offset, int length)
147                 {
148                         WriteBytes (name, type, value, offset, length, String.Empty);
149                 }
150
151                 void WriteBytes (string name, Type type, byte[] value, int offset, int length, string comment)
152                 {
153                         writer.WriteStartElement ("data");
154                         writer.WriteAttributeString ("name", name);
155
156                         if (type != null) {
157                                 writer.WriteAttributeString ("type", type.AssemblyQualifiedName);
158                                 // byte[] should never get a mimetype, otherwise MS.NET won't be able
159                                 // to parse the data.
160                                 if (type != typeof (byte[]))
161                                         writer.WriteAttributeString ("mimetype", ByteArraySerializedObjectMimeType);
162                                 writer.WriteStartElement ("value");
163                                 WriteNiceBase64 (value, offset, length);
164                         } else {
165                                 writer.WriteAttributeString ("mimetype", BinSerializedObjectMimeType);
166                                 writer.WriteStartElement ("value");
167                                 writer.WriteBase64 (value, offset, length);
168                         }
169
170                         writer.WriteEndElement ();
171
172                         if (!(comment == null || comment.Equals (String.Empty))) {
173                                 writer.WriteStartElement ("comment");
174                                 writer.WriteString (comment);
175                                 writer.WriteEndElement ();
176                         }
177                         
178                         writer.WriteEndElement ();
179                 }
180
181                 void WriteBytes (string name, Type type, byte [] value, string comment)
182                 {
183                         WriteBytes (name, type, value, 0, value.Length, comment);
184                 }
185
186                 void WriteString (string name, string value)
187                 {
188                         WriteString (name, value, null);
189                 }
190                 void WriteString (string name, string value, Type type)
191                 {
192                         WriteString (name, value, type, String.Empty);
193                 }
194                 void WriteString (string name, string value, Type type, string comment)
195                 {
196                         writer.WriteStartElement ("data");
197                         writer.WriteAttributeString ("name", name);
198                         if (type != null)
199                                 writer.WriteAttributeString ("type", type.AssemblyQualifiedName);
200                         writer.WriteStartElement ("value");
201                         writer.WriteString (value);
202                         writer.WriteEndElement ();
203                         if (!(comment == null || comment.Equals (String.Empty))) {
204                                 writer.WriteStartElement ("comment");
205                                 writer.WriteString (comment);
206                                 writer.WriteEndElement ();
207                         }
208                         writer.WriteEndElement ();
209                         writer.WriteWhitespace ("\n  ");
210                 }
211
212                 public void AddResource (string name, byte [] value)
213                 {
214                         if (name == null)
215                                 throw new ArgumentNullException ("name");
216
217                         if (value == null)
218                                 throw new ArgumentNullException ("value");
219
220                         if (written)
221                                 throw new InvalidOperationException ("The resource is already generated.");
222
223                         if (writer == null)
224                                 InitWriter ();
225
226                         WriteBytes (name, value.GetType (), value, null);
227                 }
228
229                 public void AddResource (string name, object value)
230                 {
231                         AddResource (name, value, String.Empty);
232                 }
233
234                 private void AddResource (string name, object value, string comment)
235                 {
236                         if (value is string) {
237                                 AddResource (name, (string) value, comment);
238                                 return;
239                         }
240
241                         if (name == null)
242                                 throw new ArgumentNullException ("name");
243
244                         if (value != null && !value.GetType ().IsSerializable)
245                                         throw new InvalidOperationException (String.Format ("The element '{0}' of type '{1}' is not serializable.", name, value.GetType ().Name));
246
247                         if (written)
248                                 throw new InvalidOperationException ("The resource is already generated.");
249
250                         if (writer == null)
251                                 InitWriter ();
252
253                         if (value is byte[]) {
254                                 WriteBytes (name, value.GetType (), (byte []) value, comment);
255                                 return;
256                         }
257
258                         if (value == null) {
259                                 // nulls written as ResXNullRef
260                                 WriteString (name, "", typeof (ResXNullRef), comment);
261                                 return;
262                         }
263
264                         TypeConverter converter = TypeDescriptor.GetConverter (value);
265                         if (value is ResXFileRef) {
266                                 ResXFileRef fileRef = ProcessFileRefBasePath ((ResXFileRef) value);     
267                                 string str = (string) converter.ConvertToInvariantString (fileRef);
268                                 WriteString (name, str, value.GetType (), comment);
269                                 return;
270                         }
271
272                         if (converter != null && converter.CanConvertTo (typeof (string)) && converter.CanConvertFrom (typeof (string))) {
273                                 string str = (string) converter.ConvertToInvariantString (value);
274                                 WriteString (name, str, value.GetType (), comment);
275                                 return;
276                         }
277                         
278                         if (converter != null && converter.CanConvertTo (typeof (byte[])) && converter.CanConvertFrom (typeof (byte[]))) {
279                                 byte[] b = (byte[]) converter.ConvertTo (value, typeof (byte[]));
280                                 WriteBytes (name, value.GetType (), b, comment);
281                                 return;
282                         }
283                         
284                         MemoryStream ms = new MemoryStream ();
285                         BinaryFormatter fmt = new BinaryFormatter ();
286                         try {
287                                 fmt.Serialize (ms, value);
288                         } catch (Exception e) {
289                                 throw new InvalidOperationException ("Cannot add a " + value.GetType () +
290                                                                      "because it cannot be serialized: " +
291                                                                      e.Message);
292                         }
293
294                         WriteBytes (name, null, ms.GetBuffer (), 0, (int) ms.Length, comment);
295                         ms.Close ();
296                 }
297                 
298                 public void AddResource (string name, string value)
299                 {
300                         AddResource (name, value, string.Empty);
301                 }
302
303                 private void AddResource (string name, string value, string comment)
304                 {
305                         if (name == null)
306                                 throw new ArgumentNullException ("name");
307
308                         if (value == null)
309                                 throw new ArgumentNullException ("value");
310
311                         if (written)
312                                 throw new InvalidOperationException ("The resource is already generated.");
313
314                         if (writer == null)
315                                 InitWriter ();
316
317                         WriteString (name, value, null, comment);
318                 }
319
320                 [MonoTODO ("Stub, not implemented")]
321                 public virtual void AddAlias (string aliasName, AssemblyName assemblyName)
322                 {
323                 }
324                 
325                 public void AddResource (ResXDataNode node)
326                 {
327                         if (node == null)
328                                 throw new ArgumentNullException ("node");
329
330                         if (writer == null)
331                                 InitWriter ();
332
333                         if (node.IsWritable)
334                                 WriteWritableNode (node);
335                         else if (node.FileRef != null)
336                                 AddResource (node.Name, node.FileRef, node.Comment);
337                         else 
338                                 AddResource (node.Name, node.GetValue ((AssemblyName []) null), node.Comment);
339                 }
340
341                 ResXFileRef ProcessFileRefBasePath (ResXFileRef fileRef)
342                 {
343                         if (String.IsNullOrEmpty (BasePath))
344                                 return fileRef;
345
346                         string newPath = AbsoluteToRelativePath (BasePath, fileRef.FileName);
347                         return new ResXFileRef (newPath, fileRef.TypeName, fileRef.TextFileEncoding);
348                 }
349
350                 static bool IsSeparator (char ch)
351                 {
352                         return ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar || ch == Path.VolumeSeparatorChar;
353                 }
354                 //adapted from MonoDevelop.Core
355                 unsafe static string AbsoluteToRelativePath (string baseDirectoryPath, string absPath)
356                 {
357                         if (string.IsNullOrEmpty (baseDirectoryPath))
358                                 return absPath;
359
360                         baseDirectoryPath = baseDirectoryPath.TrimEnd (Path.DirectorySeparatorChar);
361
362                         fixed (char* bPtr = baseDirectoryPath, aPtr = absPath) {
363                                 var bEnd = bPtr + baseDirectoryPath.Length;
364                                 var aEnd = aPtr + absPath.Length;
365                                 char* lastStartA = aEnd;
366                                 char* lastStartB = bEnd;
367                                 
368                                 int indx = 0;
369                                 // search common base path
370                                 var a = aPtr;
371                                 var b = bPtr;
372                                 while (a < aEnd) {
373                                         if (*a != *b)
374                                                 break;
375                                         if (IsSeparator (*a)) {
376                                                 indx++;
377                                                 lastStartA = a + 1;
378                                                 lastStartB = b; 
379                                         }
380                                         a++;
381                                         b++;
382                                         if (b >= bEnd) {
383                                                 if (a >= aEnd || IsSeparator (*a)) {
384                                                         indx++;
385                                                         lastStartA = a + 1;
386                                                         lastStartB = b;
387                                                 }
388                                                 break;
389                                         }
390                                 }
391                                 if (indx == 0) 
392                                         return absPath;
393                                 
394                                 if (lastStartA >= aEnd)
395                                         return ".";
396                                 
397                                 // handle case a: some/path b: some/path/deeper...
398                                 if (a >= aEnd) {
399                                         if (IsSeparator (*b)) {
400                                                 lastStartA = a + 1;
401                                                 lastStartB = b;
402                                         }
403                                 }
404                                 
405                                 // look how many levels to go up into the base path
406                                 int goUpCount = 0;
407                                 while (lastStartB < bEnd) {
408                                         if (IsSeparator (*lastStartB))
409                                                 goUpCount++;
410                                         lastStartB++;
411                                 }
412                                 var size = goUpCount * 2 + goUpCount + aEnd - lastStartA;
413                                 var result = new char [size];
414                                 fixed (char* rPtr = result) {
415                                         // go paths up
416                                         var r = rPtr;
417                                         for (int i = 0; i < goUpCount; i++) {
418                                                 *(r++) = '.';
419                                                 *(r++) = '.';
420                                                 *(r++) = Path.DirectorySeparatorChar;
421                                         }
422                                         // copy the remaining absulute path
423                                         while (lastStartA < aEnd)
424                                                 *(r++) = *(lastStartA++);
425                                 }
426                                 return new string (result);
427                         }
428                 }
429
430                 // avoids instantiating objects
431                 void WriteWritableNode (ResXDataNode node)
432                 {
433                         writer.WriteStartElement ("data");
434                         writer.WriteAttributeString ("name", node.Name);
435                         if (!(node.type == null || node.type.Equals (String.Empty)))
436                                 writer.WriteAttributeString ("type", node.type);
437                         if (!(node.mime_type == null || node.mime_type.Equals (String.Empty)))
438                                 writer.WriteAttributeString ("mimetype", node.mime_type);
439                         writer.WriteStartElement ("value");
440                         writer.WriteString (node.dataString);
441                         writer.WriteEndElement ();
442                         if (!(node.Comment == null || node.Comment.Equals (String.Empty))) {
443                                 writer.WriteStartElement ("comment");
444                                 writer.WriteString (node.Comment);
445                                 writer.WriteEndElement ();
446                         }
447                         writer.WriteEndElement ();
448                         writer.WriteWhitespace ("\n  ");
449                 }               
450
451                 public void AddMetadata (string name, string value)
452                 {
453                         if (name == null)
454                                 throw new ArgumentNullException ("name");
455
456                         if (value == null)
457                                 throw new ArgumentNullException ("value");
458
459                         if (written)
460                                 throw new InvalidOperationException ("The resource is already generated.");
461
462                         if (writer == null)
463                                 InitWriter ();
464
465                         writer.WriteStartElement ("metadata");
466                         writer.WriteAttributeString ("name", name);
467                         writer.WriteAttributeString ("xml:space", "preserve");
468                         
469                         writer.WriteElementString ("value", value);
470                         
471                         writer.WriteEndElement ();
472                 }
473
474                 public void AddMetadata (string name, byte[] value)
475                 {
476                         if (name == null)
477                                 throw new ArgumentNullException ("name");
478
479                         if (value == null)
480                                 throw new ArgumentNullException ("value");
481
482                         if (written)
483                                 throw new InvalidOperationException ("The resource is already generated.");
484
485                         if (writer == null)
486                                 InitWriter ();
487
488                         writer.WriteStartElement ("metadata");
489                         writer.WriteAttributeString ("name", name);
490
491                         writer.WriteAttributeString ("type", value.GetType ().AssemblyQualifiedName);
492                         
493                         writer.WriteStartElement ("value");
494                         WriteNiceBase64 (value, 0, value.Length);
495                         writer.WriteEndElement ();
496
497                         writer.WriteEndElement ();
498                 }
499                 
500                 public void AddMetadata (string name, object value)
501                 {
502                         if (value is string) {
503                                 AddMetadata (name, (string)value);
504                                 return;
505                         }
506
507                         if (value is byte[]) {
508                                 AddMetadata (name, (byte[])value);
509                                 return;
510                         }
511
512                         if (name == null)
513                                 throw new ArgumentNullException ("name");
514
515                         if (value == null)
516                                 throw new ArgumentNullException ("value");
517
518                         if (!value.GetType ().IsSerializable)
519                                 throw new InvalidOperationException (String.Format ("The element '{0}' of type '{1}' is not serializable.", name, value.GetType ().Name));
520
521                         if (written)
522                                 throw new InvalidOperationException ("The resource is already generated.");
523
524                         if (writer == null)
525                                 InitWriter ();
526
527                         Type type = value.GetType ();
528                         
529                         TypeConverter converter = TypeDescriptor.GetConverter (value);
530                         if (converter != null && converter.CanConvertTo (typeof (string)) && converter.CanConvertFrom (typeof (string))) {
531                                 string str = (string)converter.ConvertToInvariantString (value);
532                                 writer.WriteStartElement ("metadata");
533                                 writer.WriteAttributeString ("name", name);
534                                 if (type != null)
535                                         writer.WriteAttributeString ("type", type.AssemblyQualifiedName);
536                                 writer.WriteStartElement ("value");
537                                 writer.WriteString (str);
538                                 writer.WriteEndElement ();
539                                 writer.WriteEndElement ();
540                                 writer.WriteWhitespace ("\n  ");
541                                 return;
542                         }
543
544                         if (converter != null && converter.CanConvertTo (typeof (byte[])) && converter.CanConvertFrom (typeof (byte[]))) {
545                                 byte[] b = (byte[])converter.ConvertTo (value, typeof (byte[]));
546                                 writer.WriteStartElement ("metadata");
547                                 writer.WriteAttributeString ("name", name);
548
549                                 if (type != null) {
550                                         writer.WriteAttributeString ("type", type.AssemblyQualifiedName);
551                                         writer.WriteAttributeString ("mimetype", ByteArraySerializedObjectMimeType);
552                                         writer.WriteStartElement ("value");
553                                         WriteNiceBase64 (b, 0, b.Length);
554                                 } else {
555                                         writer.WriteAttributeString ("mimetype", BinSerializedObjectMimeType);
556                                         writer.WriteStartElement ("value");
557                                         writer.WriteBase64 (b, 0, b.Length);
558                                 }
559
560                                 writer.WriteEndElement ();
561                                 writer.WriteEndElement ();
562                                 return;
563                         }
564
565                         MemoryStream ms = new MemoryStream ();
566                         BinaryFormatter fmt = new BinaryFormatter ();
567                         try {
568                                 fmt.Serialize (ms, value);
569                         } catch (Exception e) {
570                                 throw new InvalidOperationException ("Cannot add a " + value.GetType () +
571                                                                      "because it cannot be serialized: " +
572                                                                      e.Message);
573                         }
574
575                         writer.WriteStartElement ("metadata");
576                         writer.WriteAttributeString ("name", name);
577
578                         if (type != null) {
579                                 writer.WriteAttributeString ("type", type.AssemblyQualifiedName);
580                                 writer.WriteAttributeString ("mimetype", ByteArraySerializedObjectMimeType);
581                                 writer.WriteStartElement ("value");
582                                 WriteNiceBase64 (ms.GetBuffer (), 0, ms.GetBuffer ().Length);
583                         } else {
584                                 writer.WriteAttributeString ("mimetype", BinSerializedObjectMimeType);
585                                 writer.WriteStartElement ("value");
586                                 writer.WriteBase64 (ms.GetBuffer (), 0, ms.GetBuffer ().Length);
587                         }
588
589                         writer.WriteEndElement ();
590                         writer.WriteEndElement ();
591                         ms.Close ();
592                 }
593
594                 public void Close ()
595                 {
596                         if (!written) {
597                                 Generate ();
598                         }
599
600                         if (writer != null) {
601                                 writer.Close ();
602                                 stream = null;
603                                 filename = null;
604                                 textwriter = null;
605                         }
606                 }
607                 
608                 public virtual void Dispose ()
609                 {
610                         Dispose(true);
611                         GC.SuppressFinalize(this);
612                 }
613
614                 public void Generate ()
615                 {
616                         if (written)
617                                 throw new InvalidOperationException ("The resource is already generated.");
618
619                         written = true;
620                         writer.WriteEndElement ();
621                         writer.Flush ();
622                 }
623
624                 protected virtual void Dispose (bool disposing)
625                 {
626                         if (disposing)
627                                 Close();
628                 }
629
630                 static string schema = @"
631   <xsd:schema id='root' xmlns='' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:msdata='urn:schemas-microsoft-com:xml-msdata'>
632     <xsd:element name='root' msdata:IsDataSet='true'>
633       <xsd:complexType>
634         <xsd:choice maxOccurs='unbounded'>
635           <xsd:element name='data'>
636             <xsd:complexType>
637               <xsd:sequence>
638                 <xsd:element name='value' type='xsd:string' minOccurs='0' msdata:Ordinal='1' />
639                 <xsd:element name='comment' type='xsd:string' minOccurs='0' msdata:Ordinal='2' />
640               </xsd:sequence>
641               <xsd:attribute name='name' type='xsd:string' msdata:Ordinal='1' />
642               <xsd:attribute name='type' type='xsd:string' msdata:Ordinal='3' />
643               <xsd:attribute name='mimetype' type='xsd:string' msdata:Ordinal='4' />
644             </xsd:complexType>
645           </xsd:element>
646           <xsd:element name='resheader'>
647             <xsd:complexType>
648               <xsd:sequence>
649                 <xsd:element name='value' type='xsd:string' minOccurs='0' msdata:Ordinal='1' />
650               </xsd:sequence>
651               <xsd:attribute name='name' type='xsd:string' use='required' />
652             </xsd:complexType>
653           </xsd:element>
654         </xsd:choice>
655       </xsd:complexType>
656     </xsd:element>
657   </xsd:schema>
658 ".Replace ("'", "\"");
659
660                 #region Public Properties
661                 public string BasePath {
662                         get { return base_path; }
663                         set { base_path = value; }
664                 }
665                 #endregion
666         }
667 }