Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*************************************************************************
3 : *
4 : * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5 : *
6 : * Copyright 2000, 2010 Oracle and/or its affiliates.
7 : *
8 : * OpenOffice.org - a multi-platform office productivity suite
9 : *
10 : * This file is part of OpenOffice.org.
11 : *
12 : * OpenOffice.org is free software: you can redistribute it and/or modify
13 : * it under the terms of the GNU Lesser General Public License version 3
14 : * only, as published by the Free Software Foundation.
15 : *
16 : * OpenOffice.org is distributed in the hope that it will be useful,
17 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 : * GNU Lesser General Public License version 3 for more details
20 : * (a copy is included in the LICENSE file that accompanied this code).
21 : *
22 : * You should have received a copy of the GNU Lesser General Public License
23 : * version 3 along with OpenOffice.org. If not, see
24 : * <http://www.openoffice.org/license.html>
25 : * for a copy of the LGPLv3 License.
26 : *
27 : ************************************************************************/
28 :
29 : /**************************************************************************
30 : TODO
31 : **************************************************************************
32 :
33 : *************************************************************************/
34 :
35 : #include <set>
36 : #include <com/sun/star/beans/Property.hpp>
37 : #include <com/sun/star/beans/PropertyAttribute.hpp>
38 : #include <com/sun/star/beans/PropertyValue.hpp>
39 : #include <com/sun/star/ucb/CommandInfo.hpp>
40 : #include <com/sun/star/ucb/ContentInfo.hpp>
41 : #include <com/sun/star/ucb/OpenCommandArgument2.hpp>
42 : #include <com/sun/star/ucb/InsertCommandArgument.hpp>
43 : #include <com/sun/star/ucb/PostCommandArgument2.hpp>
44 : #include <com/sun/star/ucb/TransferInfo.hpp>
45 : #include <com/sun/star/uno/Sequence.hxx>
46 : #include <com/sun/star/util/DateTime.hpp>
47 : #include <com/sun/star/ucb/Link.hpp>
48 : #include <com/sun/star/ucb/Lock.hpp>
49 : #include <com/sun/star/ucb/LockEntry.hpp>
50 : #include "webdavcontent.hxx"
51 : #include "webdavprovider.hxx"
52 : #include "DAVSession.hxx"
53 : #include "ContentProperties.hxx"
54 :
55 : using namespace com::sun::star;
56 : using namespace webdav_ucp;
57 :
58 : //=========================================================================
59 : //
60 : // ContentProvider implementation.
61 : //
62 : //=========================================================================
63 :
64 0 : bool ContentProvider::getProperty(
65 : const OUString & rPropName, beans::Property & rProp, bool bStrict )
66 : {
67 0 : if ( !m_pProps )
68 : {
69 0 : osl::MutexGuard aGuard( m_aMutex );
70 0 : if ( !m_pProps )
71 : {
72 0 : m_pProps = new PropertyMap;
73 :
74 : //////////////////////////////////////////////////////////////
75 : // Fill map of known properties...
76 : //////////////////////////////////////////////////////////////
77 :
78 : // Mandatory UCB properties.
79 : m_pProps->insert(
80 : beans::Property(
81 : OUString( "ContentType" ),
82 : -1,
83 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
84 : beans::PropertyAttribute::BOUND
85 0 : | beans::PropertyAttribute::READONLY ) );
86 :
87 : m_pProps->insert(
88 : beans::Property(
89 : OUString( "IsDocument" ),
90 : -1,
91 0 : getCppuBooleanType(),
92 : beans::PropertyAttribute::BOUND
93 0 : | beans::PropertyAttribute::READONLY ) );
94 :
95 : m_pProps->insert(
96 : beans::Property(
97 : OUString( "IsFolder" ),
98 : -1,
99 0 : getCppuBooleanType(),
100 : beans::PropertyAttribute::BOUND
101 0 : | beans::PropertyAttribute::READONLY ) );
102 :
103 : m_pProps->insert(
104 : beans::Property(
105 : OUString( "Title" ),
106 : -1,
107 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
108 0 : beans::PropertyAttribute::BOUND ) );
109 :
110 : // Optional UCB properties.
111 :
112 : m_pProps->insert(
113 : beans::Property(
114 : OUString( "DateCreated" ),
115 : -1,
116 0 : getCppuType( static_cast< const util::DateTime * >( 0 ) ),
117 : beans::PropertyAttribute::BOUND
118 0 : | beans::PropertyAttribute::READONLY ) );
119 :
120 : m_pProps->insert(
121 : beans::Property(
122 : OUString( "DateModified" ),
123 : -1,
124 0 : getCppuType( static_cast< const util::DateTime * >( 0 ) ),
125 : beans::PropertyAttribute::BOUND
126 0 : | beans::PropertyAttribute::READONLY ) );
127 :
128 : m_pProps->insert(
129 : beans::Property(
130 : OUString( "MediaType" ),
131 : -1,
132 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
133 : beans::PropertyAttribute::BOUND
134 0 : | beans::PropertyAttribute::READONLY ) );
135 :
136 : m_pProps->insert(
137 : beans::Property(
138 : OUString( "Size" ),
139 : -1,
140 0 : getCppuType( static_cast< const sal_Int64 * >( 0 ) ),
141 : beans::PropertyAttribute::BOUND
142 0 : | beans::PropertyAttribute::READONLY ) );
143 :
144 : m_pProps->insert(
145 : beans::Property(
146 : OUString( "BaseURI" ),
147 : -1,
148 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
149 : beans::PropertyAttribute::BOUND
150 0 : | beans::PropertyAttribute::READONLY ) );
151 :
152 : m_pProps->insert(
153 : beans::Property(
154 : OUString(
155 : "CreatableContentsInfo" ),
156 : -1,
157 : getCppuType( static_cast<
158 0 : const uno::Sequence< ucb::ContentInfo > * >( 0 ) ),
159 : beans::PropertyAttribute::BOUND
160 0 : | beans::PropertyAttribute::READONLY ) );
161 :
162 : // Standard DAV properties.
163 :
164 : m_pProps->insert(
165 : beans::Property(
166 : DAVProperties::CREATIONDATE,
167 : -1,
168 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
169 : beans::PropertyAttribute::BOUND
170 0 : | beans::PropertyAttribute::READONLY ) );
171 :
172 : m_pProps->insert(
173 : beans::Property(
174 : DAVProperties::DISPLAYNAME,
175 : -1,
176 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
177 0 : beans::PropertyAttribute::BOUND ) );
178 :
179 : m_pProps->insert(
180 : beans::Property(
181 : DAVProperties::GETCONTENTLANGUAGE,
182 : -1,
183 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
184 : beans::PropertyAttribute::BOUND
185 0 : | beans::PropertyAttribute::READONLY ) );
186 :
187 : m_pProps->insert(
188 : beans::Property(
189 : DAVProperties::GETCONTENTLENGTH,
190 : -1,
191 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
192 : beans::PropertyAttribute::BOUND
193 0 : | beans::PropertyAttribute::READONLY ) );
194 :
195 : m_pProps->insert(
196 : beans::Property(
197 : DAVProperties::GETCONTENTTYPE ,
198 : -1,
199 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
200 : beans::PropertyAttribute::BOUND
201 0 : | beans::PropertyAttribute::READONLY ) );
202 :
203 : m_pProps->insert(
204 : beans::Property(
205 : DAVProperties::GETETAG,
206 : -1,
207 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
208 : beans::PropertyAttribute::BOUND
209 0 : | beans::PropertyAttribute::READONLY ) );
210 :
211 : m_pProps->insert(
212 : beans::Property(
213 : DAVProperties::GETLASTMODIFIED,
214 : -1,
215 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
216 : beans::PropertyAttribute::BOUND
217 0 : | beans::PropertyAttribute::READONLY ) );
218 :
219 : m_pProps->insert(
220 : beans::Property(
221 : DAVProperties::LOCKDISCOVERY,
222 : -1,
223 : getCppuType( static_cast<
224 0 : const uno::Sequence< ucb::Lock > * >( 0 ) ),
225 : beans::PropertyAttribute::BOUND
226 0 : | beans::PropertyAttribute::READONLY ) );
227 :
228 : m_pProps->insert(
229 : beans::Property(
230 : DAVProperties::RESOURCETYPE,
231 : -1,
232 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
233 : beans::PropertyAttribute::BOUND
234 0 : | beans::PropertyAttribute::READONLY ) );
235 :
236 : m_pProps->insert(
237 : beans::Property(
238 : DAVProperties::SOURCE,
239 : -1,
240 : getCppuType( static_cast<
241 0 : const uno::Sequence< ucb::Link > * >( 0 ) ),
242 0 : beans::PropertyAttribute::BOUND ) );
243 :
244 : m_pProps->insert(
245 : beans::Property(
246 : DAVProperties::SUPPORTEDLOCK,
247 : -1,
248 : getCppuType( static_cast<
249 : const uno::Sequence<
250 0 : ucb::LockEntry > * >( 0 ) ),
251 : beans::PropertyAttribute::BOUND
252 0 : | beans::PropertyAttribute::READONLY ) );
253 :
254 : m_pProps->insert(
255 : beans::Property(
256 : DAVProperties::EXECUTABLE,
257 : -1,
258 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
259 0 : beans::PropertyAttribute::BOUND ) );
260 0 : }
261 : }
262 :
263 : //////////////////////////////////////////////////////////////
264 : // Lookup property.
265 : //////////////////////////////////////////////////////////////
266 :
267 0 : beans::Property aProp;
268 0 : aProp.Name = rPropName;
269 0 : const PropertyMap::const_iterator it = m_pProps->find( aProp );
270 0 : if ( it != m_pProps->end() )
271 : {
272 0 : rProp = (*it);
273 : }
274 : else
275 : {
276 0 : if ( bStrict )
277 0 : return false;
278 :
279 : // All unknown props are treated as:
280 0 : rProp = beans::Property(
281 : rPropName,
282 : - 1,
283 0 : getCppuType( static_cast< const OUString * >( 0 ) ),
284 0 : beans::PropertyAttribute::BOUND );
285 : }
286 :
287 0 : return true;
288 : }
289 :
290 : //=========================================================================
291 : //
292 : // Content implementation.
293 : //
294 : //=========================================================================
295 :
296 : // virtual
297 0 : uno::Sequence< beans::Property > Content::getProperties(
298 : const uno::Reference< ucb::XCommandEnvironment > & xEnv )
299 : {
300 : sal_Bool bTransient;
301 : SAL_WNODEPRECATED_DECLARATIONS_PUSH
302 0 : std::auto_ptr< DAVResourceAccess > xResAccess;
303 0 : std::auto_ptr< ContentProperties > xCachedProps;
304 : SAL_WNODEPRECATED_DECLARATIONS_POP
305 0 : rtl::Reference< ContentProvider > xProvider;
306 :
307 : {
308 0 : osl::Guard< osl::Mutex > aGuard( m_aMutex );
309 :
310 0 : bTransient = m_bTransient;
311 0 : xResAccess.reset( new DAVResourceAccess( *m_xResAccess.get() ) );
312 0 : if ( m_xCachedProps.get() )
313 : xCachedProps.reset(
314 0 : new ContentProperties( *m_xCachedProps.get() ) );
315 0 : xProvider.set( m_pProvider );
316 : }
317 :
318 : typedef std::set< OUString > StringSet;
319 0 : StringSet aPropSet;
320 :
321 : // No server access for just created (not yet committed) objects.
322 : // Only a minimal set of properties supported at this stage.
323 0 : if ( !bTransient )
324 : {
325 : // Obtain all properties supported for this resource from server.
326 : try
327 : {
328 0 : std::vector< DAVResourceInfo > props;
329 0 : xResAccess->PROPFIND( DAVZERO, props, xEnv );
330 :
331 : // Note: vector always contains exactly one resource info, because
332 : // we used a depth of DAVZERO for PROPFIND.
333 0 : aPropSet.insert( (*props.begin()).properties.begin(),
334 0 : (*props.begin()).properties.end() );
335 : }
336 0 : catch ( DAVException const & )
337 : {
338 : }
339 : }
340 :
341 : // Add DAV properties, map DAV properties to UCB properties.
342 0 : sal_Bool bHasCreationDate = sal_False; // creationdate <-> DateCreated
343 0 : sal_Bool bHasGetLastModified = sal_False; // getlastmodified <-> DateModified
344 0 : sal_Bool bHasGetContentType = sal_False; // getcontenttype <-> MediaType
345 0 : sal_Bool bHasGetContentLength = sal_False; // getcontentlength <-> Size
346 :
347 0 : sal_Bool bHasContentType = sal_False;
348 0 : sal_Bool bHasIsDocument = sal_False;
349 0 : sal_Bool bHasIsFolder = sal_False;
350 0 : sal_Bool bHasTitle = sal_False;
351 0 : sal_Bool bHasBaseURI = sal_False;
352 0 : sal_Bool bHasDateCreated = sal_False;
353 0 : sal_Bool bHasDateModified = sal_False;
354 0 : sal_Bool bHasMediaType = sal_False;
355 0 : sal_Bool bHasSize = sal_False;
356 0 : sal_Bool bHasCreatableInfos = sal_False;
357 :
358 : {
359 0 : std::set< OUString >::const_iterator it = aPropSet.begin();
360 0 : std::set< OUString >::const_iterator end = aPropSet.end();
361 0 : while ( it != end )
362 : {
363 0 : if ( !bHasCreationDate &&
364 0 : ( (*it) == DAVProperties::CREATIONDATE ) )
365 : {
366 0 : bHasCreationDate = sal_True;
367 : }
368 0 : else if ( !bHasGetLastModified &&
369 0 : ( (*it) == DAVProperties::GETLASTMODIFIED ) )
370 : {
371 0 : bHasGetLastModified = sal_True;
372 : }
373 0 : else if ( !bHasGetContentType &&
374 0 : ( (*it) == DAVProperties::GETCONTENTTYPE ) )
375 : {
376 0 : bHasGetContentType = sal_True;
377 : }
378 0 : else if ( !bHasGetContentLength &&
379 0 : ( (*it) == DAVProperties::GETCONTENTLENGTH ) )
380 : {
381 0 : bHasGetContentLength = sal_True;
382 : }
383 0 : else if ( !bHasContentType && (*it) == "ContentType" )
384 : {
385 0 : bHasContentType = sal_True;
386 : }
387 0 : else if ( !bHasIsDocument && (*it) == "IsDocument" )
388 : {
389 0 : bHasIsDocument = sal_True;
390 : }
391 0 : else if ( !bHasIsFolder && (*it) == "IsFolder" )
392 : {
393 0 : bHasIsFolder = sal_True;
394 : }
395 0 : else if ( !bHasTitle && (*it) == "Title" )
396 : {
397 0 : bHasTitle = sal_True;
398 : }
399 0 : else if ( !bHasBaseURI && (*it) == "BaseURI" )
400 : {
401 0 : bHasBaseURI = sal_True;
402 : }
403 0 : else if ( !bHasDateCreated && (*it) == "DateCreated" )
404 : {
405 0 : bHasDateCreated = sal_True;
406 : }
407 0 : else if ( !bHasDateModified && (*it) == "DateModified" )
408 : {
409 0 : bHasDateModified = sal_True;
410 : }
411 0 : else if ( !bHasMediaType && (*it) == "MediaType" )
412 : {
413 0 : bHasMediaType = sal_True;
414 : }
415 0 : else if ( !bHasSize && (*it) == "Size" )
416 : {
417 0 : bHasSize = sal_True;
418 : }
419 0 : else if ( !bHasCreatableInfos && (*it) == "CreatableContentsInfo" )
420 : {
421 0 : bHasCreatableInfos = sal_True;
422 : }
423 0 : ++it;
424 : }
425 : }
426 :
427 : // Add mandatory properties.
428 0 : if ( !bHasContentType )
429 : aPropSet.insert(
430 0 : OUString( "ContentType" ) );
431 :
432 0 : if ( !bHasIsDocument )
433 : aPropSet.insert(
434 0 : OUString( "IsDocument" ) );
435 :
436 0 : if ( !bHasIsFolder )
437 : aPropSet.insert(
438 0 : OUString( "IsFolder" ) );
439 :
440 0 : if ( !bHasTitle )
441 : {
442 : // Always present since it can be calculated from content's URI.
443 : aPropSet.insert(
444 0 : OUString( "Title" ) );
445 : }
446 :
447 : // Add optional properties.
448 :
449 0 : if ( !bHasBaseURI )
450 : {
451 : // Always present since it can be calculated from content's URI.
452 : aPropSet.insert(
453 0 : OUString( "BaseURI" ) );
454 : }
455 :
456 0 : if ( !bHasDateCreated && bHasCreationDate )
457 : aPropSet.insert(
458 0 : OUString( "DateCreated" ) );
459 :
460 0 : if ( !bHasDateModified && bHasGetLastModified )
461 : aPropSet.insert(
462 0 : OUString( "DateModified" ) );
463 :
464 0 : if ( !bHasMediaType && bHasGetContentType )
465 : aPropSet.insert(
466 0 : OUString( "MediaType" ) );
467 :
468 0 : if ( !bHasSize && bHasGetContentLength )
469 : aPropSet.insert(
470 0 : OUString( "Size" ) );
471 :
472 0 : if ( !bHasCreatableInfos )
473 : aPropSet.insert(
474 : OUString(
475 0 : "CreatableContentsInfo" ) );
476 :
477 : // Add cached properties, if present and still missing.
478 0 : if ( xCachedProps.get() )
479 : {
480 : const std::set< OUString >::const_iterator set_end
481 0 : = aPropSet.end();
482 :
483 : SAL_WNODEPRECATED_DECLARATIONS_PUSH
484 : const std::auto_ptr< PropertyValueMap > & xProps
485 0 : = xCachedProps->getProperties();
486 : SAL_WNODEPRECATED_DECLARATIONS_POP
487 :
488 0 : PropertyValueMap::const_iterator map_it = xProps->begin();
489 0 : const PropertyValueMap::const_iterator map_end = xProps->end();
490 :
491 0 : while ( map_it != map_end )
492 : {
493 0 : if ( aPropSet.find( (*map_it).first ) == set_end )
494 0 : aPropSet.insert( (*map_it).first );
495 :
496 0 : ++map_it;
497 : }
498 : }
499 :
500 : // std::set -> uno::Sequence
501 0 : sal_Int32 nCount = aPropSet.size();
502 0 : uno::Sequence< beans::Property > aProperties( nCount );
503 :
504 0 : std::set< OUString >::const_iterator it = aPropSet.begin();
505 0 : beans::Property aProp;
506 :
507 0 : for ( sal_Int32 n = 0; n < nCount; ++n, ++it )
508 : {
509 0 : xProvider->getProperty( (*it), aProp );
510 0 : aProperties[ n ] = aProp;
511 : }
512 :
513 0 : return aProperties;
514 : }
515 :
516 : //=========================================================================
517 : // virtual
518 0 : uno::Sequence< ucb::CommandInfo > Content::getCommands(
519 : const uno::Reference< ucb::XCommandEnvironment > & xEnv )
520 : {
521 0 : osl::Guard< osl::Mutex > aGuard( m_aMutex );
522 :
523 0 : uno::Sequence< ucb::CommandInfo > aCmdInfo( 8 );
524 :
525 : ///////////////////////////////////////////////////////////////
526 : // Mandatory commands
527 : ///////////////////////////////////////////////////////////////
528 :
529 0 : aCmdInfo[ 0 ] =
530 : ucb::CommandInfo(
531 : OUString( "getCommandInfo" ),
532 : -1,
533 0 : getCppuVoidType() );
534 0 : aCmdInfo[ 1 ] =
535 : ucb::CommandInfo(
536 : OUString( "getPropertySetInfo" ),
537 : -1,
538 0 : getCppuVoidType() );
539 0 : aCmdInfo[ 2 ] =
540 : ucb::CommandInfo(
541 : OUString( "getPropertyValues" ),
542 : -1,
543 : getCppuType( static_cast<
544 0 : uno::Sequence< beans::Property > * >( 0 ) ) );
545 0 : aCmdInfo[ 3 ] =
546 : ucb::CommandInfo(
547 : OUString( "setPropertyValues" ),
548 : -1,
549 : getCppuType( static_cast<
550 0 : uno::Sequence< beans::PropertyValue > * >( 0 ) ) );
551 :
552 : ///////////////////////////////////////////////////////////////
553 : // Optional standard commands
554 : ///////////////////////////////////////////////////////////////
555 :
556 0 : aCmdInfo[ 4 ] =
557 : ucb::CommandInfo(
558 : OUString( "delete" ),
559 : -1,
560 0 : getCppuBooleanType() );
561 0 : aCmdInfo[ 5 ] =
562 : ucb::CommandInfo(
563 : OUString( "insert" ),
564 : -1,
565 : getCppuType( static_cast<
566 0 : ucb::InsertCommandArgument * >( 0 ) ) );
567 0 : aCmdInfo[ 6 ] =
568 : ucb::CommandInfo(
569 : OUString( "open" ),
570 : -1,
571 : getCppuType( static_cast<
572 0 : ucb::OpenCommandArgument2 * >( 0 ) ) );
573 :
574 : ///////////////////////////////////////////////////////////////
575 : // New commands
576 : ///////////////////////////////////////////////////////////////
577 :
578 0 : aCmdInfo[ 7 ] =
579 : ucb::CommandInfo(
580 : OUString( "post" ),
581 : -1,
582 : getCppuType( static_cast<
583 0 : ucb::PostCommandArgument2 * >( 0 ) ) );
584 :
585 0 : sal_Bool bFolder = sal_False;
586 :
587 : try
588 : {
589 0 : bFolder = isFolder( xEnv );
590 : }
591 0 : catch ( uno::Exception const & )
592 : {
593 0 : return aCmdInfo;
594 : }
595 :
596 0 : sal_Bool bSupportsLocking = supportsExclusiveWriteLock( xEnv );
597 :
598 0 : sal_Int32 nPos = aCmdInfo.getLength();
599 0 : sal_Int32 nMoreCmds = ( bFolder ? 2 : 0 ) + ( bSupportsLocking ? 2 : 0 );
600 0 : if ( nMoreCmds )
601 0 : aCmdInfo.realloc( nPos + nMoreCmds );
602 : else
603 0 : return aCmdInfo;
604 :
605 0 : if ( bFolder )
606 : {
607 : ///////////////////////////////////////////////////////////////
608 : // Optional standard commands
609 : ///////////////////////////////////////////////////////////////
610 :
611 0 : aCmdInfo[ nPos ] =
612 : ucb::CommandInfo(
613 : OUString( "transfer" ),
614 : -1,
615 0 : getCppuType( static_cast< ucb::TransferInfo * >( 0 ) ) );
616 0 : nPos++;
617 0 : aCmdInfo[ nPos ] =
618 : ucb::CommandInfo(
619 : OUString(
620 : "createNewContent" ),
621 : -1,
622 0 : getCppuType( static_cast< ucb::ContentInfo * >( 0 ) ) );
623 0 : nPos++;
624 : }
625 : else
626 : {
627 : // no document-only commands at the moment.
628 : }
629 :
630 0 : if ( bSupportsLocking )
631 : {
632 0 : aCmdInfo[ nPos ] =
633 : ucb::CommandInfo(
634 : OUString( "lock" ),
635 : -1,
636 0 : getCppuVoidType() );
637 0 : nPos++;
638 0 : aCmdInfo[ nPos ] =
639 : ucb::CommandInfo(
640 : OUString( "unlock" ),
641 : -1,
642 0 : getCppuVoidType() );
643 0 : nPos++;
644 : }
645 0 : return aCmdInfo;
646 : }
647 :
648 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|