Статьи

Spring MVC для Atom Feeds

Как добавить каналы (Atom) в ваше веб-приложение всего с двумя классами?
Как насчет Spring MVC?

Вот мои предположения:

  • вы используете Spring Framework
  • у вас есть какой-то объект, скажем «Новости», который вы хотите опубликовать в своих каналах
  • ваша сущность «Новости» имеет creationDate, title и shortDescription
  • у вас есть некоторый репозиторий / dao, скажем «NewsRepository», который будет возвращать новости из вашей базы данных
  • Вы хотите написать как можно меньше
  • Вы не хотите форматировать Atom (XML) вручную

На самом деле вам НЕ нужно использовать Spring MVC в вашем приложении. Если вы это сделаете, перейдите к шагу 3.

Шаг 1: добавьте зависимость Spring MVC в ваше приложение

С Maven это будет:

1
2
3
4
5
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>3.1.0.RELEASE</version>
</dependency>

Шаг 2: добавьте Spring MVC DispatcherServlet

С web.xml это будет:

01
02
03
04
05
06
07
08
09
10
11
12
13
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/feed</url-pattern>
</servlet-mapping>

Обратите внимание, я установил шаблон URL на «/ feed», что означает, что я не хочу, чтобы Spring MVC обрабатывал любые другие URL в моем приложении (я использую другую веб-среду для остальной части приложения). Я также даю ему совершенно новый contextConfigLocation, где хранится только конфигурация mvc.

Помните, что когда вы добавляете DispatcherServlet в приложение, в котором уже есть Spring (например, из ContextLoaderListener), ваш контекст наследуется от глобального, поэтому вам не следует создавать bean-компоненты, которые существуют там снова, или включать xml, который их определяет. Следите за тем, как контекст Spring поднимается дважды, и обратитесь к документации по Spring или сервлету, чтобы понять, что радует.

Шаг 3. добавить ROME — библиотека для обработки формата Atom

С Maven это:

1
2
3
4
5
<dependency>
    <groupId>net.java.dev.rome</groupId>
    <artifactId>rome</artifactId>
    <version>1.0.0</version>
</dependency>

Шаг 4. написать свой очень простой контроллер

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Controller
public class FeedController {
    static final String LAST_UPDATE_VIEW_KEY = 'lastUpdate';
    static final String NEWS_VIEW_KEY = 'news';
    private NewsRepository newsRepository;
    private String viewName;
 
    protected FeedController() {} //required by cglib
 
    public FeedController(NewsRepository newsRepository, String viewName) {
        notNull(newsRepository); hasText(viewName);
        this.newsRepository = newsRepository;
        this.viewName = viewName;
    }
 
    @RequestMapping(value = '/feed', method = RequestMethod.GET)       
    @Transactional
    public ModelAndView feed() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName(viewName);
        List<News> news = newsRepository.fetchPublished();
        modelAndView.addObject(NEWS_VIEW_KEY, news);
        modelAndView.addObject(LAST_UPDATE_VIEW_KEY, getCreationDateOfTheLast(news));
        return modelAndView;
    }
 
    private Date getCreationDateOfTheLast(List<News> news) {
        if(news.size() > 0) {
            return news.get(0).getCreationDate();
        }
        return new Date(0);
    }
}

И вот тест для него, на случай, если вы хотите скопировать и вставить (а кто нет?):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@RunWith(MockitoJUnitRunner.class)
public class FeedControllerShould {
    @Mock private NewsRepository newsRepository;
    private Date FORMER_ENTRY_CREATION_DATE = new Date(1);
    private Date LATTER_ENTRY_CREATION_DATE = new Date(2);
    private ArrayList<News> newsList;
    private FeedController feedController;
 
    @Before
    public void prepareNewsList() {
        News news1 = new News().title('title1').creationDate(FORMER_ENTRY_CREATION_DATE);
        News news2 = new News().title('title2').creationDate(LATTER_ENTRY_CREATION_DATE);
        newsList = newArrayList(news2, news1);
    }
 
    @Before
    public void prepareFeedController() {
        feedController = new FeedController(newsRepository, 'viewName');
    }
 
    @Test
    public void returnViewWithNews() {
        //given
        given(newsRepository.fetchPublished()).willReturn(newsList);
         
        //when
        ModelAndView modelAndView = feedController.feed();
         
        //then
        assertThat(modelAndView.getModel())
                .includes(entry(FeedController.NEWS_VIEW_KEY, newsList));
    }
 
    @Test
    public void returnViewWithLastUpdateTime() {
        //given
        given(newsRepository.fetchPublished()).willReturn(newsList);
 
        //when
        ModelAndView modelAndView = feedController.feed();
 
        //then
        assertThat(modelAndView.getModel())
                .includes(entry(FeedController.LAST_UPDATE_VIEW_KEY, LATTER_ENTRY_CREATION_DATE));
    }
 
    @Test
    public void returnTheBeginningOfTimeAsLastUpdateInViewWhenListIsEmpty() {
        //given
        given(newsRepository.fetchPublished()).willReturn(new ArrayList<News>());
 
        //when
        ModelAndView modelAndView = feedController.feed();
 
        //then
        assertThat(modelAndView.getModel())
                .includes(entry(FeedController.LAST_UPDATE_VIEW_KEY, new Date(0)));
    }
}

Обратите внимание: здесь я использую fest-assert и mockito. Зависимости:

01
02
03
04
05
06
07
08
09
10
11
12
<dependency>
 <groupId>org.easytesting</groupId>
 <artifactId>fest-assert</artifactId>
 <version>1.4</version>
 <scope>test</scope>
</dependency>
<dependency>
 <groupId>org.mockito</groupId>
 <artifactId>mockito-all</artifactId>
 <version>1.8.5</version>
 <scope>test</scope>
</dependency>

Шаг 5. напишите свой очень простой вид

Вот где происходит все волшебное форматирование. Обязательно ознакомьтесь со всеми методами класса Entry, так как вы можете захотеть использовать / fill довольно много.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.springframework.web.servlet.view.feed.AbstractAtomFeedView;
[...]
 
public class AtomFeedView extends AbstractAtomFeedView {
    private String feedId = 'tag:yourFantastiSiteName';
    private String title = 'yourFantastiSiteName: news';
    private String newsAbsoluteUrl = 'http://yourfanstasticsiteUrl.com/news/';
 
    @Override
    protected void buildFeedMetadata(Map<String, Object> model, Feed feed, HttpServletRequest request) {
        feed.setId(feedId);
        feed.setTitle(title);
        setUpdatedIfNeeded(model, feed);
    }
 
    private void setUpdatedIfNeeded(Map<String, Object> model, Feed feed) {
        @SuppressWarnings('unchecked')
        Date lastUpdate = (Date)model.get(FeedController.LAST_UPDATE_VIEW_KEY);
        if (feed.getUpdated() == null || lastUpdate != null || lastUpdate.compareTo(feed.getUpdated()) > 0) {
            feed.setUpdated(lastUpdate);
        }
    }
 
    @Override
    protected List<Entry> buildFeedEntries(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        @SuppressWarnings('unchecked')
        List<News> newsList = (List<News>)model.get(FeedController.NEWS_VIEW_KEY);
        List<Entry> entries = new ArrayList<Entry>();
        for (News news : newsList) {
            addEntry(entries, news);
        }
        return entries;
    }
 
    private void addEntry(List<Entry> entries, News news) {
        Entry entry = new Entry();
        entry.setId(feedId + ', ' + news.getId());
        entry.setTitle(news.getTitle());
        entry.setUpdated(news.getCreationDate());
        entry = setSummary(news, entry);
        entry = setLink(news, entry);
        entries.add(entry);
    }
 
    private Entry setSummary(News news, Entry entry) {
        Content summary = new Content();
        summary.setValue(news.getShortDescription());
        entry.setSummary(summary);
        return entry;
    }
 
    private Entry setLink(News news, Entry entry) {
        Link link = new Link();
        link.setType('text/html');
        link.setHref(newsAbsoluteUrl + news.getId()); //because I have a different controller to show news at http://yourfanstasticsiteUrl.com/news/ID
        entry.setAlternateLinks(newArrayList(link));
        return entry;
    }
 
}

Шаг 6. добавьте ваши классы в контекст Spring

Я использую XML-подход. потому что я старый и я люблю XML. Нет, серьезно, я использую xml, потому что я могу несколько раз объявить FeedController с разными представлениями (RSS 1.0, RSS 2.0 и т. Д.).

Так что это вышеупомянутая весна-mvc.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version='1.0' encoding='UTF-8'?>
 
    <bean class='org.springframework.web.servlet.view.ContentNegotiatingViewResolver'>
        <property name='mediaTypes'>
            <map>
                <entry key='atom' value='application/atom+xml'/>
                <entry key='html' value='text/html'/>
            </map>
        </property>
        <property name='viewResolvers'>
            <list>
                <bean class='org.springframework.web.servlet.view.BeanNameViewResolver'/>
            </list>
        </property>
    </bean>
 
    <bean class='eu.margiel.pages.confitura.feed.FeedController'>
        <constructor-arg index='0' ref='newsRepository'/>
        <constructor-arg index='1' value='atomFeedView'/>
    </bean>
 
    <bean id='atomFeedView' class='eu.margiel.pages.confitura.feed.AtomFeedView'/>
</beans>

И вы сделали.

Меня несколько раз просили поместить весь рабочий код в публичный репозиторий, так что на этот раз все наоборот. Я опишу вещи, которые я уже опубликовал, и вы можете получить коммит из битбакета .

Ссылка: Atom Feeds с Spring MVC от нашего партнера JCG Якуба Набрдалика в блоге Solid Craft .